ГЛОБАЛЬНЫЙ РЕФАКТОРИНГ АРХИТЕКТУРЫ - Рефакторинг этапов Stage 3.8 - юи
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { apiClient } from "./api/client";
|
||||
import { AssistantSamPanel } from "./components/AssistantSamPanel";
|
||||
import { AutoRunsHistoryPanel } from "./components/AutoRunsHistoryPanel";
|
||||
@@ -7,6 +7,7 @@ import { ConnectionPanel } from "./components/ConnectionPanel";
|
||||
import { HistoryPanel } from "./components/HistoryPanel";
|
||||
import { MetricsPanel } from "./components/MetricsPanel";
|
||||
import { OutputPanel } from "./components/OutputPanel";
|
||||
import { PanelFrame } from "./components/PanelFrame";
|
||||
import { PromptPanel } from "./components/PromptPanel";
|
||||
import { QueryPanel } from "./components/QueryPanel";
|
||||
import { RuntimePanel } from "./components/RuntimePanel";
|
||||
@@ -14,6 +15,7 @@ import { DEFAULT_CONNECTION, DEFAULT_PROMPTS, DEFAULT_QUERY } from "./state/defa
|
||||
import { designConfig } from "../../../designconfig";
|
||||
import type {
|
||||
AssistantConversationItem,
|
||||
AssistantAnnotationRecord,
|
||||
ConnectionState,
|
||||
HistoryItem,
|
||||
NormalizeResultState,
|
||||
@@ -32,6 +34,17 @@ const DEFAULT_UI_MODE: UiMode = "assistant";
|
||||
const AUTOLOAD_PROMPT_VERSION = "normalizer_v2_0_2";
|
||||
const ASSISTANT_PROMPT_VERSION = "address_query_runtime_v1";
|
||||
const TAB_KEYS: TabKey[] = ["normalized", "fragments", "scope", "flags", "route", "raw", "validation", "logs"];
|
||||
const DEFAULT_ASSISTANT_ANNOTATION_AUTHOR = "manual_reviewer";
|
||||
|
||||
interface AssistantCommentModalState {
|
||||
open: boolean;
|
||||
messageIndex: number;
|
||||
rating: number;
|
||||
comment: string;
|
||||
annotationAuthor: string;
|
||||
saving: boolean;
|
||||
error: string;
|
||||
}
|
||||
|
||||
function withTs(message: string): string {
|
||||
return `[${new Date().toLocaleTimeString("ru-RU")}] ${message}`;
|
||||
@@ -91,6 +104,7 @@ export default function App() {
|
||||
const [showAssistantConnectionMode, setShowAssistantConnectionMode] = useState(true);
|
||||
const [showAssistantPromptMode, setShowAssistantPromptMode] = useState(true);
|
||||
const [showAssistantChatMode, setShowAssistantChatMode] = useState(true);
|
||||
const [showAssistantCommentsMode, setShowAssistantCommentsMode] = useState(true);
|
||||
const [showAssistantSamMode, setShowAssistantSamMode] = useState(true);
|
||||
const [showDecompositionConnectionMode, setShowDecompositionConnectionMode] = useState(true);
|
||||
const [showDecompositionPromptMode, setShowDecompositionPromptMode] = useState(true);
|
||||
@@ -105,6 +119,17 @@ export default function App() {
|
||||
const [assistantBusy, setAssistantBusy] = useState(false);
|
||||
const [assistantStatus, setAssistantStatus] = useState("");
|
||||
const [assistantError, setAssistantError] = useState("");
|
||||
const [assistantAnnotations, setAssistantAnnotations] = useState<AssistantAnnotationRecord[]>([]);
|
||||
const [assistantAnnotationsBusy, setAssistantAnnotationsBusy] = useState(false);
|
||||
const [assistantCommentModal, setAssistantCommentModal] = useState<AssistantCommentModalState>({
|
||||
open: false,
|
||||
messageIndex: -1,
|
||||
rating: 3,
|
||||
comment: "",
|
||||
annotationAuthor: DEFAULT_ASSISTANT_ANNOTATION_AUTHOR,
|
||||
saving: false,
|
||||
error: ""
|
||||
});
|
||||
const presetAutoloadDoneRef = useRef(false);
|
||||
const skipPresetAutoloadRef = useRef(false);
|
||||
|
||||
@@ -172,6 +197,7 @@ export default function App() {
|
||||
showAssistantConnectionMode?: boolean;
|
||||
showAssistantPromptMode?: boolean;
|
||||
showAssistantChatMode?: boolean;
|
||||
showAssistantCommentsMode?: boolean;
|
||||
showAssistantSamMode?: boolean;
|
||||
showDecompositionConnectionMode?: boolean;
|
||||
showDecompositionPromptMode?: boolean;
|
||||
@@ -209,6 +235,9 @@ export default function App() {
|
||||
if (typeof parsed.showAssistantChatMode === "boolean") {
|
||||
setShowAssistantChatMode(parsed.showAssistantChatMode);
|
||||
}
|
||||
if (typeof parsed.showAssistantCommentsMode === "boolean") {
|
||||
setShowAssistantCommentsMode(parsed.showAssistantCommentsMode);
|
||||
}
|
||||
if (typeof parsed.showAssistantSamMode === "boolean") {
|
||||
setShowAssistantSamMode(parsed.showAssistantSamMode);
|
||||
}
|
||||
@@ -331,6 +360,7 @@ export default function App() {
|
||||
showAssistantConnectionMode,
|
||||
showAssistantPromptMode,
|
||||
showAssistantChatMode,
|
||||
showAssistantCommentsMode,
|
||||
showAssistantSamMode,
|
||||
showDecompositionConnectionMode,
|
||||
showDecompositionPromptMode,
|
||||
@@ -619,12 +649,146 @@ export default function App() {
|
||||
}
|
||||
}
|
||||
|
||||
const assistantAnnotationsByMessageId = useMemo(() => {
|
||||
const map = new Map<string, AssistantAnnotationRecord>();
|
||||
for (const item of assistantAnnotations) {
|
||||
if (item.message_id) {
|
||||
map.set(item.message_id, item);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}, [assistantAnnotations]);
|
||||
|
||||
const assistantCommentModalMessage =
|
||||
assistantCommentModal.messageIndex >= 0 ? assistantConversation[assistantCommentModal.messageIndex] ?? null : null;
|
||||
|
||||
const assistantCommentModalQuestion = useMemo(() => {
|
||||
if (assistantCommentModal.messageIndex < 0) return null;
|
||||
for (let index = assistantCommentModal.messageIndex - 1; index >= 0; index -= 1) {
|
||||
const candidate = assistantConversation[index];
|
||||
if (candidate?.role === "user") {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}, [assistantCommentModal.messageIndex, assistantConversation]);
|
||||
|
||||
async function loadAssistantAnnotationsForSession(sessionId: string): Promise<void> {
|
||||
if (!sessionId.trim()) {
|
||||
setAssistantAnnotations([]);
|
||||
return;
|
||||
}
|
||||
setAssistantAnnotationsBusy(true);
|
||||
try {
|
||||
const payload = await apiClient.loadAssistantAnnotations({
|
||||
session_id: sessionId,
|
||||
limit: 400
|
||||
});
|
||||
setAssistantAnnotations(payload.items ?? []);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
log(`Assistant annotations load error: ${message}`);
|
||||
} finally {
|
||||
setAssistantAnnotationsBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
function closeAssistantCommentModal(options?: { force?: boolean }) {
|
||||
setAssistantCommentModal((prev) => {
|
||||
if (prev.saving && !options?.force) {
|
||||
return prev;
|
||||
}
|
||||
return {
|
||||
open: false,
|
||||
messageIndex: -1,
|
||||
rating: 3,
|
||||
comment: "",
|
||||
annotationAuthor: DEFAULT_ASSISTANT_ANNOTATION_AUTHOR,
|
||||
saving: false,
|
||||
error: ""
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function openAssistantCommentModal(item: AssistantConversationItem, index: number): void {
|
||||
if (item.role !== "assistant") return;
|
||||
const sessionIdFromState = assistantSessionId.trim();
|
||||
const sessionIdFromItem = String(item.session_id ?? "").trim();
|
||||
const resolvedSessionId = sessionIdFromState || sessionIdFromItem;
|
||||
if (!resolvedSessionId) {
|
||||
setAssistantError("Сначала получите ответ ассистента в активной сессии.");
|
||||
return;
|
||||
}
|
||||
if (!sessionIdFromState && sessionIdFromItem) {
|
||||
setAssistantSessionId(sessionIdFromItem);
|
||||
}
|
||||
const existing = assistantAnnotationsByMessageId.get(item.message_id) ?? null;
|
||||
setAssistantCommentModal({
|
||||
open: true,
|
||||
messageIndex: index,
|
||||
rating: existing?.rating ?? 3,
|
||||
comment: existing?.comment ?? "",
|
||||
annotationAuthor: existing?.annotation_author ?? DEFAULT_ASSISTANT_ANNOTATION_AUTHOR,
|
||||
saving: false,
|
||||
error: ""
|
||||
});
|
||||
}
|
||||
|
||||
function canCommentAssistantMessage(item: AssistantConversationItem): boolean {
|
||||
return item.role === "assistant";
|
||||
}
|
||||
|
||||
function isAssistantMessageCommented(item: AssistantConversationItem): boolean {
|
||||
return item.role === "assistant" && assistantAnnotationsByMessageId.has(item.message_id);
|
||||
}
|
||||
|
||||
async function submitAssistantCommentModal(): Promise<void> {
|
||||
if (!assistantSessionId.trim()) {
|
||||
setAssistantCommentModal((prev) => ({ ...prev, error: "Сессия ассистента не найдена." }));
|
||||
return;
|
||||
}
|
||||
if (assistantCommentModal.messageIndex < 0) {
|
||||
return;
|
||||
}
|
||||
if (!assistantCommentModal.comment.trim()) {
|
||||
setAssistantCommentModal((prev) => ({ ...prev, error: "Добавьте комментарий." }));
|
||||
return;
|
||||
}
|
||||
|
||||
setAssistantCommentModal((prev) => ({ ...prev, saving: true, error: "" }));
|
||||
try {
|
||||
const payload = await apiClient.saveAssistantAnnotation({
|
||||
session_id: assistantSessionId,
|
||||
message_index: assistantCommentModal.messageIndex,
|
||||
rating: assistantCommentModal.rating,
|
||||
comment: assistantCommentModal.comment.trim(),
|
||||
annotation_author: assistantCommentModal.annotationAuthor.trim() || undefined
|
||||
});
|
||||
setAssistantAnnotations((prev) => {
|
||||
const next = [...prev];
|
||||
const index = next.findIndex((item) => item.annotation_id === payload.annotation.annotation_id);
|
||||
if (index >= 0) {
|
||||
next[index] = payload.annotation;
|
||||
} else {
|
||||
next.unshift(payload.annotation);
|
||||
}
|
||||
return next.sort((a, b) => Date.parse(b.updated_at) - Date.parse(a.updated_at));
|
||||
});
|
||||
closeAssistantCommentModal({ force: true });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
setAssistantCommentModal((prev) => ({ ...prev, saving: false, error: message }));
|
||||
}
|
||||
}
|
||||
|
||||
function resetAssistantSession() {
|
||||
setAssistantSessionId("");
|
||||
setAssistantConversation([]);
|
||||
setAssistantInput("");
|
||||
setAssistantStatus("");
|
||||
setAssistantError("");
|
||||
setAssistantAnnotations([]);
|
||||
closeAssistantCommentModal({ force: true });
|
||||
log("Assistant session reset.");
|
||||
}
|
||||
|
||||
@@ -664,6 +828,7 @@ export default function App() {
|
||||
setAssistantSessionId(response.session_id);
|
||||
setAssistantConversation(response.conversation);
|
||||
setAssistantStatus("Ответ готов");
|
||||
await loadAssistantAnnotationsForSession(response.session_id);
|
||||
log(`Assistant reply received: trace=${response.debug.trace_id}`);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
@@ -676,6 +841,14 @@ export default function App() {
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!assistantSessionId.trim()) {
|
||||
setAssistantAnnotations([]);
|
||||
return;
|
||||
}
|
||||
void loadAssistantAnnotationsForSession(assistantSessionId);
|
||||
}, [assistantSessionId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedRunId) {
|
||||
setRunTrace([]);
|
||||
@@ -728,6 +901,13 @@ export default function App() {
|
||||
<button type="button" className={showAssistantChatMode ? "tab active" : "tab"} onClick={() => setShowAssistantChatMode((prev) => !prev)}>
|
||||
Режим ассистента
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={showAssistantCommentsMode ? "tab active" : "tab"}
|
||||
onClick={() => setShowAssistantCommentsMode((prev) => !prev)}
|
||||
>
|
||||
Комментарии ассистента
|
||||
</button>
|
||||
<button type="button" className={showAssistantSamMode ? "tab active" : "tab"} onClick={() => setShowAssistantSamMode((prev) => !prev)}>
|
||||
SAM
|
||||
</button>
|
||||
@@ -862,10 +1042,57 @@ export default function App() {
|
||||
busy={assistantBusy}
|
||||
statusText={assistantStatus}
|
||||
errorMessage={assistantError}
|
||||
showCommentAction
|
||||
onCommentAssistantMessage={openAssistantCommentModal}
|
||||
isAssistantMessageCommented={isAssistantMessageCommented}
|
||||
canCommentAssistantMessage={canCommentAssistantMessage}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{showAssistantCommentsMode ? (
|
||||
<div className="mode-col">
|
||||
<PanelFrame className="assistant-comments-frame" title="Комментарии ассистента">
|
||||
<div className="assistant-comments-shell">
|
||||
<div className="assistant-comments-toolbar">
|
||||
<span className="muted">
|
||||
{assistantSessionId ? `session: ${assistantSessionId}` : "Сессия не запущена"}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="tab"
|
||||
onClick={() => void loadAssistantAnnotationsForSession(assistantSessionId)}
|
||||
disabled={!assistantSessionId || assistantAnnotationsBusy}
|
||||
>
|
||||
{assistantAnnotationsBusy ? "Обновляю..." : "Обновить"}
|
||||
</button>
|
||||
</div>
|
||||
<div className="assistant-comments-list">
|
||||
{!assistantSessionId ? <p className="muted">Появится после первого ответа ассистента.</p> : null}
|
||||
{assistantSessionId && assistantAnnotations.length === 0 && !assistantAnnotationsBusy ? (
|
||||
<p className="muted">Комментариев по этой сессии пока нет.</p>
|
||||
) : null}
|
||||
{assistantAnnotations.map((item) => (
|
||||
<article key={item.annotation_id} className="assistant-comment-item">
|
||||
<div className="assistant-comment-head">
|
||||
<strong>{`${"●".repeat(Math.max(1, Math.min(5, Math.round(item.rating))))}${"○".repeat(Math.max(0, 5 - Math.round(item.rating)))}`}</strong>
|
||||
<span>{new Date(item.updated_at).toLocaleString("ru-RU")}</span>
|
||||
</div>
|
||||
{item.context.question_text ? <p>Q: {item.context.question_text}</p> : null}
|
||||
{item.context.answer_text ? <p>A: {item.context.answer_text}</p> : null}
|
||||
<p>{item.comment}</p>
|
||||
<div className="assistant-comment-meta">
|
||||
{item.context.trace_id ? <span>{`trace=${item.context.trace_id}`}</span> : null}
|
||||
{item.context.reply_type ? <span>{`reply_type=${item.context.reply_type}`}</span> : null}
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</PanelFrame>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{showAssistantSamMode ? (
|
||||
<div className="mode-col">
|
||||
<AssistantSamPanel
|
||||
@@ -879,7 +1106,11 @@ export default function App() {
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!showAssistantConnectionMode && !showAssistantPromptMode && !showAssistantChatMode && !showAssistantSamMode ? (
|
||||
{!showAssistantConnectionMode &&
|
||||
!showAssistantPromptMode &&
|
||||
!showAssistantChatMode &&
|
||||
!showAssistantCommentsMode &&
|
||||
!showAssistantSamMode ? (
|
||||
<div className="mode-columns-empty">Все панели режима ассистента скрыты. Включите нужные блоки справа в шапке.</div>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -999,6 +1230,89 @@ export default function App() {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{assistantCommentModal.open ? (
|
||||
<div
|
||||
className="autoruns-comment-modal-backdrop"
|
||||
onClick={(event) => {
|
||||
if (event.target === event.currentTarget) {
|
||||
closeAssistantCommentModal();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="autoruns-comment-modal">
|
||||
<h3>Комментарий к ответу ассистента</h3>
|
||||
<p className="muted">Эта разметка хранится отдельно от комментариев автопрогонов.</p>
|
||||
|
||||
{assistantCommentModalQuestion ? (
|
||||
<details className="autoruns-prompt-details" open>
|
||||
<summary>Вопрос пользователя</summary>
|
||||
<p className="autoruns-comment-quote">{assistantCommentModalQuestion.text}</p>
|
||||
</details>
|
||||
) : null}
|
||||
{assistantCommentModalMessage ? (
|
||||
<details className="autoruns-prompt-details" open>
|
||||
<summary>Ответ ассистента</summary>
|
||||
<p className="autoruns-comment-quote">{assistantCommentModalMessage.text}</p>
|
||||
</details>
|
||||
) : null}
|
||||
|
||||
<div className="autoruns-rating-row" role="group" aria-label="Рейтинг ответа">
|
||||
{[1, 2, 3, 4, 5].map((value) => (
|
||||
<button
|
||||
key={value}
|
||||
type="button"
|
||||
className={assistantCommentModal.rating >= value ? "autoruns-rating-dot active" : "autoruns-rating-dot"}
|
||||
onClick={() => setAssistantCommentModal((prev) => ({ ...prev, rating: value }))}
|
||||
disabled={assistantCommentModal.saving}
|
||||
aria-label={`Оценка ${value}`}
|
||||
>
|
||||
{assistantCommentModal.rating >= value ? "●" : "○"}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="autoruns-form-grid">
|
||||
<label>
|
||||
Автор комментария
|
||||
<input
|
||||
value={assistantCommentModal.annotationAuthor}
|
||||
onChange={(event) => setAssistantCommentModal((prev) => ({ ...prev, annotationAuthor: event.target.value }))}
|
||||
placeholder="manual_reviewer"
|
||||
disabled={assistantCommentModal.saving}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label>
|
||||
Комментарий
|
||||
<textarea
|
||||
value={assistantCommentModal.comment}
|
||||
onChange={(event) => setAssistantCommentModal((prev) => ({ ...prev, comment: event.target.value }))}
|
||||
placeholder="Что именно не так в ответе и что проверить."
|
||||
rows={4}
|
||||
disabled={assistantCommentModal.saving}
|
||||
/>
|
||||
</label>
|
||||
|
||||
{assistantCommentModal.error ? <p className="error-text">{assistantCommentModal.error}</p> : null}
|
||||
|
||||
<div className="button-row">
|
||||
<button type="button" onClick={() => void submitAssistantCommentModal()} disabled={assistantCommentModal.saving}>
|
||||
{assistantCommentModal.saving ? "Сохраняю..." : "Готово"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="tab"
|
||||
onClick={() => closeAssistantCommentModal()}
|
||||
disabled={assistantCommentModal.saving}
|
||||
>
|
||||
Отмена
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ import type {
|
||||
AutoRunDialogResponse,
|
||||
AutoRunHistoryResponse,
|
||||
AutoRunPostAnalysisResponse,
|
||||
AssistantAnnotationsResponse,
|
||||
AssistantAnnotationRecord,
|
||||
AssistantMessageResultState,
|
||||
AssistantConversationItem,
|
||||
ConnectionState,
|
||||
@@ -314,6 +316,30 @@ export const apiClient = {
|
||||
return request(`/assistant/session/${sessionId}`);
|
||||
},
|
||||
|
||||
async loadAssistantAnnotations(input?: {
|
||||
session_id?: string;
|
||||
limit?: number;
|
||||
}): Promise<AssistantAnnotationsResponse> {
|
||||
const params = new URLSearchParams();
|
||||
if (input?.session_id) params.set("session_id", input.session_id);
|
||||
if (typeof input?.limit === "number") params.set("limit", String(input.limit));
|
||||
const query = params.toString();
|
||||
return request(`/assistant/annotations${query ? `?${query}` : ""}`);
|
||||
},
|
||||
|
||||
async saveAssistantAnnotation(input: {
|
||||
session_id: string;
|
||||
message_index: number;
|
||||
rating: number;
|
||||
comment: string;
|
||||
annotation_author?: string;
|
||||
}): Promise<{ ok: boolean; annotation: AssistantAnnotationRecord }> {
|
||||
return request("/assistant/annotations", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(input)
|
||||
});
|
||||
},
|
||||
|
||||
async loadAutoRunsHistory(input?: {
|
||||
from?: string;
|
||||
to?: string;
|
||||
|
||||
@@ -16,6 +16,10 @@ interface AssistantPanelProps {
|
||||
busy: boolean;
|
||||
statusText: string;
|
||||
errorMessage: string;
|
||||
showCommentAction?: boolean;
|
||||
onCommentAssistantMessage?: (item: AssistantConversationItem, index: number) => void;
|
||||
isAssistantMessageCommented?: (item: AssistantConversationItem, index: number) => boolean;
|
||||
canCommentAssistantMessage?: (item: AssistantConversationItem, index: number) => boolean;
|
||||
}
|
||||
|
||||
function roleLabel(role: AssistantConversationItem["role"]): string {
|
||||
@@ -61,6 +65,15 @@ async function copyTextToClipboard(text: string): Promise<boolean> {
|
||||
return copied;
|
||||
}
|
||||
|
||||
function CommentBubbleIcon({ commented }: { commented: boolean }) {
|
||||
const className = commented ? "comment-icon-svg commented" : "comment-icon-svg";
|
||||
return (
|
||||
<svg className={className} viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
||||
<path d="M5 6.5h14v9H11.5l-4.5 3v-3H5z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function AssistantPanel({
|
||||
sessionId,
|
||||
conversation,
|
||||
@@ -72,7 +85,11 @@ export function AssistantPanel({
|
||||
onClear,
|
||||
busy,
|
||||
statusText,
|
||||
errorMessage
|
||||
errorMessage,
|
||||
showCommentAction = false,
|
||||
onCommentAssistantMessage,
|
||||
isAssistantMessageCommented,
|
||||
canCommentAssistantMessage
|
||||
}: AssistantPanelProps) {
|
||||
const listRef = useRef<HTMLDivElement | null>(null);
|
||||
const stickToBottomRef = useRef(true);
|
||||
@@ -162,11 +179,45 @@ export function AssistantPanel({
|
||||
</div>
|
||||
|
||||
<div ref={listRef} className="assistant-chat-list" onScroll={handleChatScroll}>
|
||||
{conversation.map((item) => (
|
||||
{conversation.map((item, index) => {
|
||||
const commentEnabled =
|
||||
item.role === "assistant" &&
|
||||
showCommentAction &&
|
||||
typeof onCommentAssistantMessage === "function" &&
|
||||
(typeof canCommentAssistantMessage === "function" ? canCommentAssistantMessage(item, index) : true);
|
||||
const commented =
|
||||
item.role === "assistant" && typeof isAssistantMessageCommented === "function"
|
||||
? isAssistantMessageCommented(item, index)
|
||||
: false;
|
||||
return (
|
||||
<article key={item.message_id} className={`assistant-msg ${item.role}`}>
|
||||
<header className="assistant-msg-head">
|
||||
<strong>{roleLabel(item.role)}</strong>
|
||||
<span>{shortTime(item.created_at)}</span>
|
||||
<div className="assistant-msg-head-main">
|
||||
<strong>{roleLabel(item.role)}</strong>
|
||||
<span>{shortTime(item.created_at)}</span>
|
||||
</div>
|
||||
{item.role === "assistant" && showCommentAction ? (
|
||||
<div className="assistant-msg-head-actions">
|
||||
<button
|
||||
type="button"
|
||||
className={commented ? "autoruns-comment-icon assistant-comment-btn commented" : "autoruns-comment-icon assistant-comment-btn"}
|
||||
onClick={() => onCommentAssistantMessage?.(item, index)}
|
||||
disabled={!commentEnabled}
|
||||
title={
|
||||
commentEnabled
|
||||
? "Комментировать ответ ассистента"
|
||||
: "Комментарий недоступен для этого сообщения"
|
||||
}
|
||||
aria-label={
|
||||
commentEnabled
|
||||
? "Комментировать ответ ассистента"
|
||||
: "Комментарий недоступен для этого сообщения"
|
||||
}
|
||||
>
|
||||
<CommentBubbleIcon commented={commented} />
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</header>
|
||||
<div className="assistant-msg-body">{item.text}</div>
|
||||
{item.role === "assistant" && item.debug ? (
|
||||
@@ -176,13 +227,15 @@ export function AssistantPanel({
|
||||
</details>
|
||||
) : null}
|
||||
</article>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="assistant-compose">
|
||||
<label className="full-width">
|
||||
Сообщение
|
||||
<textarea
|
||||
className="assistant-input-textarea"
|
||||
value={inputValue}
|
||||
onChange={(event) => onInputChange(event.target.value)}
|
||||
rows={4}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState, type SyntheticEvent } from "react";
|
||||
import { apiClient } from "../api/client";
|
||||
import type {
|
||||
AssistantConversationItem,
|
||||
AssistantAnnotationRecord,
|
||||
AsyncEvalRunJob,
|
||||
AutoGenHistoryRecord,
|
||||
AutoGenMode,
|
||||
@@ -68,6 +69,16 @@ interface CommentModalState {
|
||||
error: string;
|
||||
}
|
||||
|
||||
interface AssistantLiveCommentModalState {
|
||||
open: boolean;
|
||||
messageIndex: number;
|
||||
rating: number;
|
||||
comment: string;
|
||||
annotationAuthor: string;
|
||||
saving: boolean;
|
||||
error: string;
|
||||
}
|
||||
|
||||
interface AutoGenSettingsState {
|
||||
mode: AutoGenMode;
|
||||
count: number;
|
||||
@@ -117,6 +128,7 @@ function buildDefaultPersonalityPrompts(
|
||||
interface AutoRunsUiConfig {
|
||||
filters?: Partial<AutoRunsFilters>;
|
||||
analysisDate?: string;
|
||||
autogenPersonalityPromptHeight?: number;
|
||||
autoGenSettings?: {
|
||||
mode?: AutoGenMode;
|
||||
count?: number;
|
||||
@@ -143,6 +155,11 @@ function normalizeAnalysisDateInput(value: string): string {
|
||||
return /^\d{4}-\d{2}-\d{2}$/.test(normalized) ? normalized : "";
|
||||
}
|
||||
|
||||
function clampAutogenPromptHeight(value: number | null | undefined): number {
|
||||
const numeric = typeof value === "number" && Number.isFinite(value) ? Math.trunc(value) : 160;
|
||||
return Math.max(110, Math.min(520, numeric));
|
||||
}
|
||||
|
||||
function dateToInputValue(date: Date): string {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||
@@ -391,9 +408,6 @@ function CommentBubbleIcon({ commented }: { commented: boolean }) {
|
||||
return (
|
||||
<svg className={className} viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
||||
<path d="M5 6.5h14v9H11.5l-4.5 3v-3H5z" />
|
||||
<circle className="comment-icon-dot" cx="9" cy="11" r="1.05" />
|
||||
<circle className="comment-icon-dot" cx="12" cy="11" r="1.05" />
|
||||
<circle className="comment-icon-dot" cx="15" cy="11" r="1.05" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -462,6 +476,7 @@ export function AutoRunsHistoryPanel({
|
||||
const [errorText, setErrorText] = useState("");
|
||||
const [assistantLiveSessionId, setAssistantLiveSessionId] = useState("");
|
||||
const [assistantLiveConversation, setAssistantLiveConversation] = useState<AssistantConversationItem[]>([]);
|
||||
const [assistantLiveAnnotations, setAssistantLiveAnnotations] = useState<AssistantAnnotationRecord[]>([]);
|
||||
const [assistantLiveInput, setAssistantLiveInput] = useState("");
|
||||
const [assistantLiveUseMock, setAssistantLiveUseMock] = useState(false);
|
||||
const [assistantLiveBusy, setAssistantLiveBusy] = useState(false);
|
||||
@@ -469,6 +484,7 @@ export function AutoRunsHistoryPanel({
|
||||
const [assistantLiveError, setAssistantLiveError] = useState("");
|
||||
const [limitInput, setLimitInput] = useState(String(DEFAULT_FILTERS.limit));
|
||||
const [autogenCountInput, setAutogenCountInput] = useState(String(DEFAULT_AUTOGEN_SETTINGS.count));
|
||||
const [autogenPersonalityPromptHeight, setAutogenPersonalityPromptHeight] = useState(160);
|
||||
const [commentModal, setCommentModal] = useState<CommentModalState>({
|
||||
open: false,
|
||||
caseId: "",
|
||||
@@ -481,6 +497,15 @@ export function AutoRunsHistoryPanel({
|
||||
saving: false,
|
||||
error: ""
|
||||
});
|
||||
const [assistantLiveCommentModal, setAssistantLiveCommentModal] = useState<AssistantLiveCommentModalState>({
|
||||
open: false,
|
||||
messageIndex: -1,
|
||||
rating: 3,
|
||||
comment: "",
|
||||
annotationAuthor: "manual_reviewer",
|
||||
saving: false,
|
||||
error: ""
|
||||
});
|
||||
|
||||
const initialLoadDoneRef = useRef(false);
|
||||
const asyncJobPollTimerRef = useRef<number | null>(null);
|
||||
@@ -510,6 +535,27 @@ export function AutoRunsHistoryPanel({
|
||||
}
|
||||
return null;
|
||||
}, [commentModal.messageIndex, dialog]);
|
||||
const assistantLiveAnnotationsByMessageId = useMemo(() => {
|
||||
const map = new Map<string, AssistantAnnotationRecord>();
|
||||
for (const item of assistantLiveAnnotations) {
|
||||
if (item.message_id) {
|
||||
map.set(item.message_id, item);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}, [assistantLiveAnnotations]);
|
||||
const assistantLiveCommentModalMessage =
|
||||
assistantLiveCommentModal.messageIndex >= 0 ? assistantLiveConversation[assistantLiveCommentModal.messageIndex] ?? null : null;
|
||||
const assistantLiveCommentModalQuestion = useMemo(() => {
|
||||
if (assistantLiveCommentModal.messageIndex < 0) return null;
|
||||
for (let index = assistantLiveCommentModal.messageIndex - 1; index >= 0; index -= 1) {
|
||||
const candidate = assistantLiveConversation[index];
|
||||
if (candidate?.role === "user") {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}, [assistantLiveCommentModal.messageIndex, assistantLiveConversation]);
|
||||
|
||||
const annotationsAverageRating = useMemo(() => {
|
||||
if (visibleAnnotations.length === 0) return null;
|
||||
@@ -535,6 +581,44 @@ export function AutoRunsHistoryPanel({
|
||||
[onLog]
|
||||
);
|
||||
|
||||
const loadAssistantLiveAnnotationsForSession = useCallback(
|
||||
async (sessionIdRaw: string) => {
|
||||
const sessionId = String(sessionIdRaw ?? "").trim();
|
||||
if (!sessionId) {
|
||||
setAssistantLiveAnnotations([]);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const payload = await apiClient.loadAssistantAnnotations({
|
||||
session_id: sessionId,
|
||||
limit: 400
|
||||
});
|
||||
setAssistantLiveAnnotations(payload.items ?? []);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
log(`Assistant live annotations load error: ${message}`);
|
||||
}
|
||||
},
|
||||
[log]
|
||||
);
|
||||
|
||||
const closeAssistantLiveCommentModal = useCallback((options?: { force?: boolean }) => {
|
||||
setAssistantLiveCommentModal((prev) => {
|
||||
if (prev.saving && !options?.force) {
|
||||
return prev;
|
||||
}
|
||||
return {
|
||||
open: false,
|
||||
messageIndex: -1,
|
||||
rating: 3,
|
||||
comment: "",
|
||||
annotationAuthor: "manual_reviewer",
|
||||
saving: false,
|
||||
error: ""
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
const copyRunIdToClipboard = useCallback(
|
||||
async (event: React.SyntheticEvent, runId: string) => {
|
||||
event.stopPropagation();
|
||||
@@ -580,11 +664,13 @@ export function AutoRunsHistoryPanel({
|
||||
const resetAssistantLiveSession = useCallback(() => {
|
||||
setAssistantLiveSessionId("");
|
||||
setAssistantLiveConversation([]);
|
||||
setAssistantLiveAnnotations([]);
|
||||
setAssistantLiveInput("");
|
||||
setAssistantLiveStatus("");
|
||||
setAssistantLiveError("");
|
||||
closeAssistantLiveCommentModal({ force: true });
|
||||
log("Live-чат ассистента в истории автопрогонов сброшен.");
|
||||
}, [log]);
|
||||
}, [closeAssistantLiveCommentModal, log]);
|
||||
|
||||
const sendAssistantLiveMessage = useCallback(async () => {
|
||||
const userMessage = assistantLiveInput.trim();
|
||||
@@ -621,6 +707,7 @@ export function AutoRunsHistoryPanel({
|
||||
});
|
||||
setAssistantLiveSessionId(response.session_id);
|
||||
setAssistantLiveConversation(response.conversation);
|
||||
await loadAssistantLiveAnnotationsForSession(response.session_id);
|
||||
setAssistantLiveStatus("Ответ готов");
|
||||
log(`Live-ответ ассистента получен: trace=${response.debug.trace_id}`);
|
||||
} catch (error) {
|
||||
@@ -638,6 +725,7 @@ export function AutoRunsHistoryPanel({
|
||||
assistantLiveUseMock,
|
||||
assistantPromptVersion,
|
||||
connection,
|
||||
loadAssistantLiveAnnotationsForSession,
|
||||
log,
|
||||
prompts
|
||||
]);
|
||||
@@ -692,6 +780,20 @@ export function AutoRunsHistoryPanel({
|
||||
[autoGenSettings.count]
|
||||
);
|
||||
|
||||
const commitAutogenPromptHeight = useCallback((height: number) => {
|
||||
setAutogenPersonalityPromptHeight(clampAutogenPromptHeight(height));
|
||||
}, []);
|
||||
|
||||
const captureAutogenPromptHeight = useCallback(
|
||||
(event: SyntheticEvent<HTMLTextAreaElement>) => {
|
||||
const nextHeight = event.currentTarget.offsetHeight;
|
||||
if (Number.isFinite(nextHeight) && nextHeight > 0) {
|
||||
commitAutogenPromptHeight(nextHeight);
|
||||
}
|
||||
},
|
||||
[commitAutogenPromptHeight]
|
||||
);
|
||||
|
||||
const loadAnnotations = useCallback(async () => {
|
||||
setAnnotationsBusy(true);
|
||||
try {
|
||||
@@ -1202,6 +1304,94 @@ export function AutoRunsHistoryPanel({
|
||||
selectedRunId
|
||||
]);
|
||||
|
||||
const canCommentAssistantLiveMessage = useCallback((item: AssistantConversationItem): boolean => item.role === "assistant", []);
|
||||
|
||||
const isAssistantLiveMessageCommented = useCallback(
|
||||
(item: AssistantConversationItem): boolean => item.role === "assistant" && assistantLiveAnnotationsByMessageId.has(item.message_id),
|
||||
[assistantLiveAnnotationsByMessageId]
|
||||
);
|
||||
|
||||
const openAssistantLiveCommentModal = useCallback(
|
||||
(item: AssistantConversationItem, index: number) => {
|
||||
if (item.role !== "assistant") {
|
||||
return;
|
||||
}
|
||||
const sessionIdFromState = assistantLiveSessionId.trim();
|
||||
const sessionIdFromItem = String(item.session_id ?? "").trim();
|
||||
const resolvedSessionId = sessionIdFromState || sessionIdFromItem;
|
||||
if (!resolvedSessionId) {
|
||||
setAssistantLiveError("Сначала получите ответ ассистента в активной сессии.");
|
||||
return;
|
||||
}
|
||||
if (!sessionIdFromState && sessionIdFromItem) {
|
||||
setAssistantLiveSessionId(sessionIdFromItem);
|
||||
}
|
||||
const existing = assistantLiveAnnotationsByMessageId.get(item.message_id) ?? null;
|
||||
setAssistantLiveError("");
|
||||
setAssistantLiveCommentModal({
|
||||
open: true,
|
||||
messageIndex: index,
|
||||
rating: existing?.rating ?? 3,
|
||||
comment: existing?.comment ?? "",
|
||||
annotationAuthor: existing?.annotation_author ?? "manual_reviewer",
|
||||
saving: false,
|
||||
error: ""
|
||||
});
|
||||
},
|
||||
[assistantLiveAnnotationsByMessageId, assistantLiveSessionId]
|
||||
);
|
||||
|
||||
const submitAssistantLiveCommentModal = useCallback(async () => {
|
||||
if (assistantLiveCommentModal.messageIndex < 0) {
|
||||
return;
|
||||
}
|
||||
if (!assistantLiveCommentModal.comment.trim()) {
|
||||
setAssistantLiveCommentModal((prev) => ({ ...prev, error: "Добавьте комментарий." }));
|
||||
return;
|
||||
}
|
||||
|
||||
const modalMessage = assistantLiveConversation[assistantLiveCommentModal.messageIndex] ?? null;
|
||||
const sessionId =
|
||||
assistantLiveSessionId.trim() || (modalMessage?.role === "assistant" ? String(modalMessage.session_id ?? "").trim() : "");
|
||||
if (!sessionId) {
|
||||
setAssistantLiveCommentModal((prev) => ({ ...prev, error: "Сессия ассистента не найдена." }));
|
||||
return;
|
||||
}
|
||||
|
||||
setAssistantLiveCommentModal((prev) => ({ ...prev, saving: true, error: "" }));
|
||||
try {
|
||||
const payload = await apiClient.saveAssistantAnnotation({
|
||||
session_id: sessionId,
|
||||
message_index: assistantLiveCommentModal.messageIndex,
|
||||
rating: assistantLiveCommentModal.rating,
|
||||
comment: assistantLiveCommentModal.comment.trim(),
|
||||
annotation_author: assistantLiveCommentModal.annotationAuthor.trim() || undefined
|
||||
});
|
||||
setAssistantLiveAnnotations((prev) => {
|
||||
const next = [...prev];
|
||||
const index = next.findIndex((item) => item.annotation_id === payload.annotation.annotation_id);
|
||||
if (index >= 0) {
|
||||
next[index] = payload.annotation;
|
||||
} else {
|
||||
next.unshift(payload.annotation);
|
||||
}
|
||||
return next.sort((a, b) => Date.parse(b.updated_at) - Date.parse(a.updated_at));
|
||||
});
|
||||
closeAssistantLiveCommentModal({ force: true });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
setAssistantLiveCommentModal((prev) => ({ ...prev, saving: false, error: message }));
|
||||
}
|
||||
}, [
|
||||
assistantLiveCommentModal.annotationAuthor,
|
||||
assistantLiveCommentModal.comment,
|
||||
assistantLiveCommentModal.messageIndex,
|
||||
assistantLiveCommentModal.rating,
|
||||
assistantLiveConversation,
|
||||
assistantLiveSessionId,
|
||||
closeAssistantLiveCommentModal
|
||||
]);
|
||||
|
||||
const applyLocalAnnotationPatch = useCallback((annotation: AutoRunAnnotationRecord) => {
|
||||
setAnnotations((prev) =>
|
||||
prev.map((item) =>
|
||||
@@ -1316,6 +1506,14 @@ export function AutoRunsHistoryPanel({
|
||||
setAutogenCountInput(String(autoGenSettings.count));
|
||||
}, [autoGenSettings.count]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!assistantLiveSessionId.trim()) {
|
||||
setAssistantLiveAnnotations([]);
|
||||
return;
|
||||
}
|
||||
void loadAssistantLiveAnnotationsForSession(assistantLiveSessionId);
|
||||
}, [assistantLiveSessionId, loadAssistantLiveAnnotationsForSession]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeAsyncJob) return;
|
||||
const liveRunId = toLiveRunId(activeAsyncJob.job_id);
|
||||
@@ -1375,6 +1573,9 @@ export function AutoRunsHistoryPanel({
|
||||
if (typeof parsed.analysisDate === "string") {
|
||||
setAnalysisDate(normalizeAnalysisDateInput(parsed.analysisDate));
|
||||
}
|
||||
if (typeof parsed.autogenPersonalityPromptHeight === "number") {
|
||||
setAutogenPersonalityPromptHeight(clampAutogenPromptHeight(parsed.autogenPersonalityPromptHeight));
|
||||
}
|
||||
if (parsed.autoGenSettings) {
|
||||
setAutoGenSettings((prev) => {
|
||||
const nextPrompts: Record<string, string> = {
|
||||
@@ -1431,6 +1632,7 @@ export function AutoRunsHistoryPanel({
|
||||
const payload: AutoRunsUiConfig = {
|
||||
filters,
|
||||
analysisDate,
|
||||
autogenPersonalityPromptHeight,
|
||||
autoGenSettings: {
|
||||
mode: autoGenSettings.mode,
|
||||
count: autoGenSettings.count,
|
||||
@@ -1443,7 +1645,7 @@ export function AutoRunsHistoryPanel({
|
||||
hideResolvedAnnotations
|
||||
};
|
||||
localStorage.setItem(AUTORUNS_UI_CONFIG_KEY, JSON.stringify(payload));
|
||||
}, [analysisDate, annotationDecisionFilter, autoGenSettings, filters, hideResolvedAnnotations]);
|
||||
}, [analysisDate, annotationDecisionFilter, autoGenSettings, autogenPersonalityPromptHeight, filters, hideResolvedAnnotations]);
|
||||
|
||||
useEffect(() => {
|
||||
const onSave = () => {
|
||||
@@ -1596,9 +1798,6 @@ export function AutoRunsHistoryPanel({
|
||||
</div>
|
||||
|
||||
<h4>Автогенерация вопросов</h4>
|
||||
<p className="muted">
|
||||
`qwen_seed` использует текущую LLM-модель из активного контура подключения (та же модель, что и для ответов ассистента).
|
||||
</p>
|
||||
<div className="autoruns-form-grid">
|
||||
<label>
|
||||
Режим генерации
|
||||
@@ -1660,6 +1859,7 @@ export function AutoRunsHistoryPanel({
|
||||
<label className="full-width">
|
||||
Промпт личности
|
||||
<textarea
|
||||
className="autoruns-personality-prompt"
|
||||
value={autoGenSettings.personalityPrompts[autoGenSettings.personalityId] ?? ""}
|
||||
onChange={(event) =>
|
||||
setAutoGenSettings((prev) => ({
|
||||
@@ -1671,6 +1871,9 @@ export function AutoRunsHistoryPanel({
|
||||
}))
|
||||
}
|
||||
placeholder="Текст промпта для выбранной личности автогенерации"
|
||||
style={{ height: `${autogenPersonalityPromptHeight}px` }}
|
||||
onMouseUp={captureAutogenPromptHeight}
|
||||
onTouchEnd={captureAutogenPromptHeight}
|
||||
/>
|
||||
</label>
|
||||
<label className="checkbox-row">
|
||||
@@ -1698,9 +1901,6 @@ export function AutoRunsHistoryPanel({
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="muted">
|
||||
Если дата среза задана, автопрогон анализирует данные на эту дату. Если поле пустое, используется текущее состояние.
|
||||
</p>
|
||||
|
||||
<div className="button-row">
|
||||
<button type="button" disabled={autoGenBusy} onClick={() => void generateAutogenBatch()}>
|
||||
@@ -1711,7 +1911,7 @@ export function AutoRunsHistoryPanel({
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="tab"
|
||||
className="autoruns-run-launch-btn"
|
||||
disabled={autogenRunBusy || editableGeneratedQuestions.length === 0}
|
||||
onClick={() => void runAutogenCampaign()}
|
||||
>
|
||||
@@ -1764,7 +1964,7 @@ export function AutoRunsHistoryPanel({
|
||||
title="Удалить вопрос из запуска"
|
||||
aria-label="Удалить вопрос из запуска"
|
||||
>
|
||||
X
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
@@ -2071,6 +2271,10 @@ export function AutoRunsHistoryPanel({
|
||||
busy={assistantLiveBusy}
|
||||
statusText={assistantLiveStatus}
|
||||
errorMessage={assistantLiveError}
|
||||
showCommentAction
|
||||
onCommentAssistantMessage={openAssistantLiveCommentModal}
|
||||
isAssistantMessageCommented={isAssistantLiveMessageCommented}
|
||||
canCommentAssistantMessage={canCommentAssistantLiveMessage}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
@@ -2359,6 +2563,89 @@ export function AutoRunsHistoryPanel({
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{assistantLiveCommentModal.open ? (
|
||||
<div
|
||||
className="autoruns-comment-modal-backdrop"
|
||||
onClick={(event) => {
|
||||
if (event.target === event.currentTarget) {
|
||||
closeAssistantLiveCommentModal();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="autoruns-comment-modal">
|
||||
<h3>Комментарий к ответу ассистента</h3>
|
||||
<p className="muted">Комментарий сохраняется отдельно от комментариев автопрогонов.</p>
|
||||
|
||||
{assistantLiveCommentModalQuestion ? (
|
||||
<details className="autoruns-prompt-details" open>
|
||||
<summary>Вопрос пользователя</summary>
|
||||
<p className="autoruns-comment-quote">{assistantLiveCommentModalQuestion.text}</p>
|
||||
</details>
|
||||
) : null}
|
||||
{assistantLiveCommentModalMessage ? (
|
||||
<details className="autoruns-prompt-details" open>
|
||||
<summary>Ответ ассистента</summary>
|
||||
<p className="autoruns-comment-quote">{assistantLiveCommentModalMessage.text}</p>
|
||||
</details>
|
||||
) : null}
|
||||
|
||||
<div className="autoruns-rating-row" role="group" aria-label="Рейтинг ответа ассистента">
|
||||
{[1, 2, 3, 4, 5].map((value) => (
|
||||
<button
|
||||
key={value}
|
||||
type="button"
|
||||
className={assistantLiveCommentModal.rating >= value ? "autoruns-rating-dot active" : "autoruns-rating-dot"}
|
||||
onClick={() => setAssistantLiveCommentModal((prev) => ({ ...prev, rating: value }))}
|
||||
disabled={assistantLiveCommentModal.saving}
|
||||
aria-label={`Оценка ${value}`}
|
||||
>
|
||||
{assistantLiveCommentModal.rating >= value ? "●" : "○"}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="autoruns-form-grid">
|
||||
<label>
|
||||
Автор комментария
|
||||
<input
|
||||
value={assistantLiveCommentModal.annotationAuthor}
|
||||
onChange={(event) => setAssistantLiveCommentModal((prev) => ({ ...prev, annotationAuthor: event.target.value }))}
|
||||
placeholder="manual_reviewer"
|
||||
disabled={assistantLiveCommentModal.saving}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label>
|
||||
Комментарий
|
||||
<textarea
|
||||
value={assistantLiveCommentModal.comment}
|
||||
onChange={(event) => setAssistantLiveCommentModal((prev) => ({ ...prev, comment: event.target.value }))}
|
||||
placeholder="Что именно не так в ответе и что нужно исправить."
|
||||
rows={4}
|
||||
disabled={assistantLiveCommentModal.saving}
|
||||
/>
|
||||
</label>
|
||||
|
||||
{assistantLiveCommentModal.error ? <p className="error-text">{assistantLiveCommentModal.error}</p> : null}
|
||||
|
||||
<div className="button-row">
|
||||
<button type="button" onClick={() => void submitAssistantLiveCommentModal()} disabled={assistantLiveCommentModal.saving}>
|
||||
{assistantLiveCommentModal.saving ? "Сохраняю..." : "Готово"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="tab"
|
||||
onClick={() => closeAssistantLiveCommentModal()}
|
||||
disabled={assistantLiveCommentModal.saving}
|
||||
>
|
||||
Отмена
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{commentModal.open ? (
|
||||
<div
|
||||
className="autoruns-comment-modal-backdrop"
|
||||
|
||||
@@ -495,3 +495,32 @@ export interface AssistantMessageResultState {
|
||||
debug: AssistantDebugState;
|
||||
conversation: AssistantConversationItem[];
|
||||
}
|
||||
|
||||
export interface AssistantAnnotationRecord {
|
||||
annotation_id: string;
|
||||
session_id: string;
|
||||
message_id: string;
|
||||
message_index: number;
|
||||
rating: number;
|
||||
comment: string;
|
||||
annotation_author: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
context: {
|
||||
trace_id: string | null;
|
||||
reply_type: string | null;
|
||||
question_text: string | null;
|
||||
answer_text: string | null;
|
||||
};
|
||||
technical_context: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
export interface AssistantAnnotationsResponse {
|
||||
ok: boolean;
|
||||
generated_at: string;
|
||||
filters_applied: {
|
||||
session_id: string | null;
|
||||
limit: number;
|
||||
};
|
||||
items: AssistantAnnotationRecord[];
|
||||
}
|
||||
|
||||
@@ -363,6 +363,22 @@ textarea:focus {
|
||||
textarea {
|
||||
resize: vertical;
|
||||
min-height: 86px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
.assistant-input-textarea,
|
||||
.autoruns-personality-prompt {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
scrollbar-gutter: stable both-edges;
|
||||
border-bottom-right-radius: 6px;
|
||||
}
|
||||
|
||||
.assistant-input-textarea::-webkit-scrollbar-corner,
|
||||
.autoruns-personality-prompt::-webkit-scrollbar-corner {
|
||||
background: rgb(var(--rgb-surface-horizontal));
|
||||
}
|
||||
|
||||
button {
|
||||
@@ -494,6 +510,7 @@ button:disabled {
|
||||
|
||||
.assistant-msg-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 0;
|
||||
@@ -501,6 +518,35 @@ button:disabled {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.assistant-msg-head-main {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.assistant-msg-head-actions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.assistant-comment-btn {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.assistant-comment-btn:disabled {
|
||||
opacity: 0.42;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.assistant-msg.user .assistant-comment-btn {
|
||||
color: rgba(var(--rgb-active-text), 0.92);
|
||||
}
|
||||
|
||||
.assistant-msg-body {
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.35;
|
||||
@@ -539,6 +585,66 @@ button:disabled {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.assistant-comments-frame .panel-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.assistant-comments-shell {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.assistant-comments-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.assistant-comments-list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
overflow: auto;
|
||||
min-height: 0;
|
||||
padding-right: 2px;
|
||||
}
|
||||
|
||||
.assistant-comment-item {
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
background: rgb(var(--rgb-surface-horizontal));
|
||||
padding: 8px;
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.assistant-comment-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.assistant-comment-item p {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.assistant-comment-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.74rem;
|
||||
}
|
||||
|
||||
.app-root-autoruns .assistant-panel-frame .panel-header {
|
||||
position: sticky;
|
||||
top: -12px;
|
||||
@@ -733,7 +839,8 @@ button:disabled {
|
||||
width: var(--mode-column-width);
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
border: none;
|
||||
border-radius: 14px;
|
||||
background: rgb(var(--rgb-surface-main));
|
||||
@@ -1063,10 +1170,10 @@ button:disabled {
|
||||
background: transparent;
|
||||
color: rgb(var(--rgb-text-main));
|
||||
border-radius: 0;
|
||||
min-width: 20px;
|
||||
min-height: 20px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
min-width: 16px;
|
||||
min-height: 16px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
box-shadow: none;
|
||||
@@ -1074,24 +1181,37 @@ button:disabled {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
opacity: 0.92;
|
||||
}
|
||||
|
||||
.autoruns-comment-icon:hover {
|
||||
background: transparent;
|
||||
color: rgb(var(--rgb-active));
|
||||
color: rgb(var(--rgb-text-main));
|
||||
opacity: 1;
|
||||
box-shadow: none;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.autoruns-comment-icon.commented {
|
||||
color: rgb(var(--rgb-active-text));
|
||||
color: rgb(var(--rgb-active));
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.autoruns-comment-icon:focus-visible {
|
||||
outline: 1px solid rgba(var(--rgb-text-main), 0.7);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
.autoruns-comment-icon:disabled {
|
||||
opacity: 0.42;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.comment-icon-svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
width: 0.82rem;
|
||||
height: 0.82rem;
|
||||
stroke: currentColor;
|
||||
stroke-width: 1.75;
|
||||
stroke-linecap: round;
|
||||
@@ -1099,17 +1219,9 @@ button:disabled {
|
||||
fill: none;
|
||||
}
|
||||
|
||||
.comment-icon-svg .comment-icon-dot {
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.comment-icon-svg.commented {
|
||||
fill: rgb(var(--rgb-active));
|
||||
stroke: rgb(var(--rgb-active));
|
||||
}
|
||||
|
||||
.autoruns-comment-icon.commented .comment-icon-dot {
|
||||
fill: rgb(var(--rgb-active-text));
|
||||
fill: none;
|
||||
}
|
||||
|
||||
.autoruns-resolve-toggle {
|
||||
@@ -1224,6 +1336,21 @@ button:disabled {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.autoruns-run-launch-btn {
|
||||
background: rgb(var(--rgb-active));
|
||||
color: rgb(var(--rgb-active-text));
|
||||
}
|
||||
|
||||
.autoruns-run-launch-btn:hover {
|
||||
background: rgb(var(--rgb-surface-focus));
|
||||
color: rgb(var(--rgb-text-main));
|
||||
}
|
||||
|
||||
.autoruns-run-launch-btn:disabled {
|
||||
background: rgba(var(--rgb-active), 0.38);
|
||||
color: rgba(var(--rgb-active-text), 0.88);
|
||||
}
|
||||
|
||||
.autoruns-generated-questions {
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
@@ -1249,31 +1376,58 @@ button:disabled {
|
||||
}
|
||||
|
||||
.autoruns-generated-question-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
display: block;
|
||||
gap: 8px;
|
||||
border: none;
|
||||
border-radius: 9px;
|
||||
background: rgb(var(--rgb-surface-focus));
|
||||
padding: 6px 8px;
|
||||
padding: 7px 30px 7px 8px;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.autoruns-generated-question-item span {
|
||||
display: block;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.autoruns-remove-question-btn {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
flex: 0 0 auto;
|
||||
border: none;
|
||||
border-radius: 7px;
|
||||
background: rgb(var(--rgb-surface-horizontal));
|
||||
color: var(--text-main);
|
||||
min-width: 24px;
|
||||
height: 24px;
|
||||
font-size: 0.75rem;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
color: rgb(var(--rgb-text-main));
|
||||
min-width: 16px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
padding: 0;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transform: rotate(45deg);
|
||||
box-shadow: none;
|
||||
transition: color 0.15s ease;
|
||||
}
|
||||
|
||||
.autoruns-remove-question-btn:hover {
|
||||
background: transparent;
|
||||
color: rgb(var(--rgb-active-text));
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.autoruns-remove-question-btn:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.autoruns-personality-prompt {
|
||||
resize: vertical;
|
||||
min-height: 110px;
|
||||
}
|
||||
|
||||
.autoruns-comment-item {
|
||||
|
||||
Reference in New Issue
Block a user