semantic update
This commit is contained in:
@@ -12,6 +12,8 @@
|
||||
/**
|
||||
* @TIER CRITICAL
|
||||
* @PURPOSE Displays detailed logs for a specific task inline or in a modal using TaskLogPanel.
|
||||
* @PRE Needs a valid taskId to fetch logs for.
|
||||
* @POST task logs are displayed and updated in real time.
|
||||
* @UX_STATE Loading -> Shows spinner/text while fetching initial logs
|
||||
* @UX_STATE Streaming -> Displays logs with auto-scroll, real-time appending
|
||||
* @UX_STATE Error -> Shows error message with recovery option
|
||||
@@ -42,6 +44,9 @@
|
||||
let shouldShow = $derived(inline || show);
|
||||
|
||||
// [DEF:handleRealTimeLogs:Action]
|
||||
// @PURPOSE: Sync real-time logs to the current log list
|
||||
// @PRE: None
|
||||
// @POST: logs are updated with new real-time log entries
|
||||
$effect(() => {
|
||||
if (realTimeLogs && realTimeLogs.length > 0) {
|
||||
const lastLog = realTimeLogs[realTimeLogs.length - 1];
|
||||
@@ -58,11 +63,20 @@
|
||||
// [/DEF:handleRealTimeLogs:Action]
|
||||
|
||||
// [DEF:fetchLogs:Function]
|
||||
// @PURPOSE: Fetches logs for a given task ID
|
||||
// @PRE: taskId is set
|
||||
// @POST: logs are populated with API response
|
||||
async function fetchLogs() {
|
||||
if (!taskId) return;
|
||||
try {
|
||||
console.log(`[TaskLogViewer][API][fetchLogs:STARTED] id=${taskId}`);
|
||||
logs = await getTaskLogs(taskId);
|
||||
console.log(`[TaskLogViewer][API][fetchLogs:SUCCESS] id=${taskId}`);
|
||||
} catch (e) {
|
||||
console.error(
|
||||
`[TaskLogViewer][API][fetchLogs:FAILED] id=${taskId}`,
|
||||
e,
|
||||
);
|
||||
error = e.message;
|
||||
} finally {
|
||||
loading = false;
|
||||
@@ -70,13 +84,25 @@
|
||||
}
|
||||
// [/DEF:fetchLogs:Function]
|
||||
|
||||
// [DEF:handleFilterChange:Function]
|
||||
// @PURPOSE: Updates filter conditions for the log viewer
|
||||
// @PRE: event contains detail with source and level
|
||||
// @POST: Log viewer filters updated
|
||||
function handleFilterChange(event) {
|
||||
console.log("[TaskLogViewer][UI][handleFilterChange:START]");
|
||||
const { source, level } = event.detail;
|
||||
}
|
||||
// [/DEF:handleFilterChange:Function]
|
||||
|
||||
// [DEF:handleRefresh:Function]
|
||||
// @PURPOSE: Refreshes the logs by polling the API
|
||||
// @PRE: None
|
||||
// @POST: Logs refetched
|
||||
function handleRefresh() {
|
||||
console.log("[TaskLogViewer][UI][handleRefresh:START]");
|
||||
fetchLogs();
|
||||
}
|
||||
// [/DEF:handleRefresh:Function]
|
||||
|
||||
$effect(() => {
|
||||
if (shouldShow && taskId) {
|
||||
@@ -104,6 +130,11 @@
|
||||
</script>
|
||||
|
||||
{#if shouldShow}
|
||||
<!-- [DEF:showInline:Component] -->
|
||||
<!-- @PURPOSE: Shows inline logs -->
|
||||
<!-- @LAYER: UI -->
|
||||
<!-- @SEMANTICS: logs, inline -->
|
||||
<!-- @TIER: STANDARD -->
|
||||
{#if inline}
|
||||
<div class="flex flex-col h-full w-full">
|
||||
{#if loading && logs.length === 0}
|
||||
@@ -136,7 +167,13 @@
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<!-- [/DEF:showInline:Component] -->
|
||||
{:else}
|
||||
<!-- [DEF:showModal:Component] -->
|
||||
<!-- @PURPOSE: Shows modal logs -->
|
||||
<!-- @LAYER: UI -->
|
||||
<!-- @SEMANTICS: logs, modal -->
|
||||
<!-- @TIER: STANDARD -->
|
||||
<div
|
||||
class="fixed inset-0 z-50 overflow-y-auto"
|
||||
aria-labelledby="modal-title"
|
||||
@@ -199,5 +236,6 @@
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
// [/DEF:showModal:Component]
|
||||
|
||||
<!-- [/DEF:TaskLogViewer:Component] -->
|
||||
|
||||
@@ -13,6 +13,7 @@ import { api } from '../api.js';
|
||||
// @PRE: options is an object with optional report query fields.
|
||||
// @POST: Returns URL query string without leading '?'.
|
||||
export function buildReportQueryString(options = {}) {
|
||||
console.log("[reports][api][buildReportQueryString:START]");
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (options.page != null) params.append('page', String(options.page));
|
||||
@@ -40,6 +41,7 @@ export function buildReportQueryString(options = {}) {
|
||||
// @PRE: error may be Error/string/object.
|
||||
// @POST: Returns structured error object.
|
||||
export function normalizeApiError(error) {
|
||||
console.log("[reports][api][normalizeApiError:START]");
|
||||
const message =
|
||||
(error && typeof error.message === 'string' && error.message) ||
|
||||
(typeof error === 'string' && error) ||
|
||||
@@ -59,9 +61,13 @@ export function normalizeApiError(error) {
|
||||
// @POST: Returns parsed payload or structured error for UI-state mapping.
|
||||
export async function getReports(options = {}) {
|
||||
try {
|
||||
console.log("[reports][api][getReports:STARTED]", options);
|
||||
const query = buildReportQueryString(options);
|
||||
return await api.fetchApi(`/reports${query ? `?${query}` : ''}`);
|
||||
const res = await api.fetchApi(`/reports${query ? `?${query}` : ''}`);
|
||||
console.log("[reports][api][getReports:SUCCESS]", res);
|
||||
return res;
|
||||
} catch (error) {
|
||||
console.error("[reports][api][getReports:FAILED]", error);
|
||||
throw normalizeApiError(error);
|
||||
}
|
||||
}
|
||||
@@ -73,8 +79,12 @@ export async function getReports(options = {}) {
|
||||
// @POST: Returns parsed detail payload or structured error object.
|
||||
export async function getReportDetail(reportId) {
|
||||
try {
|
||||
return await api.fetchApi(`/reports/${reportId}`);
|
||||
console.log(`[reports][api][getReportDetail:STARTED] id=${reportId}`);
|
||||
const res = await api.fetchApi(`/reports/${reportId}`);
|
||||
console.log(`[reports][api][getReportDetail:SUCCESS] id=${reportId}`);
|
||||
return res;
|
||||
} catch (error) {
|
||||
console.error(`[reports][api][getReportDetail:FAILED] id=${reportId}`, error);
|
||||
throw normalizeApiError(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,35 +23,35 @@
|
||||
* @UX_TEST: NeedsConfirmation -> {click: confirm action, expected: started response with task_id}
|
||||
*/
|
||||
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { t } from '$lib/i18n';
|
||||
import Icon from '$lib/ui/Icon.svelte';
|
||||
import { openDrawerForTask } from '$lib/stores/taskDrawer.js';
|
||||
import { onMount } from "svelte";
|
||||
import { goto } from "$app/navigation";
|
||||
import { t } from "$lib/i18n";
|
||||
import Icon from "$lib/ui/Icon.svelte";
|
||||
import { openDrawerForTask } from "$lib/stores/taskDrawer.js";
|
||||
import {
|
||||
assistantChatStore,
|
||||
closeAssistantChat,
|
||||
setAssistantConversationId,
|
||||
} from '$lib/stores/assistantChat.js';
|
||||
} from "$lib/stores/assistantChat.js";
|
||||
import {
|
||||
sendAssistantMessage,
|
||||
confirmAssistantOperation,
|
||||
cancelAssistantOperation,
|
||||
getAssistantHistory,
|
||||
getAssistantConversations,
|
||||
} from '$lib/api/assistant.js';
|
||||
} from "$lib/api/assistant.js";
|
||||
|
||||
const HISTORY_PAGE_SIZE = 30;
|
||||
const CONVERSATIONS_PAGE_SIZE = 20;
|
||||
|
||||
let input = '';
|
||||
let input = "";
|
||||
let loading = false;
|
||||
let loadingHistory = false;
|
||||
let loadingMoreHistory = false;
|
||||
let loadingConversations = false;
|
||||
let messages = [];
|
||||
let conversations = [];
|
||||
let conversationFilter = 'active';
|
||||
let conversationFilter = "active";
|
||||
let activeConversationsTotal = 0;
|
||||
let archivedConversationsTotal = 0;
|
||||
let historyPage = 1;
|
||||
@@ -77,7 +77,12 @@
|
||||
const requestVersion = ++historyLoadVersion;
|
||||
loadingHistory = true;
|
||||
try {
|
||||
const history = await getAssistantHistory(1, HISTORY_PAGE_SIZE, targetConversationId, true);
|
||||
const history = await getAssistantHistory(
|
||||
1,
|
||||
HISTORY_PAGE_SIZE,
|
||||
targetConversationId,
|
||||
true,
|
||||
);
|
||||
if (requestVersion !== historyLoadVersion) {
|
||||
return;
|
||||
}
|
||||
@@ -87,13 +92,21 @@
|
||||
}));
|
||||
historyPage = 1;
|
||||
historyHasNext = Boolean(history.has_next);
|
||||
if (!targetConversationId && history.conversation_id && history.conversation_id !== conversationId) {
|
||||
if (
|
||||
!targetConversationId &&
|
||||
history.conversation_id &&
|
||||
history.conversation_id !== conversationId
|
||||
) {
|
||||
setAssistantConversationId(history.conversation_id);
|
||||
}
|
||||
initialized = true;
|
||||
console.log('[AssistantChatPanel][Coherence:OK] History loaded');
|
||||
// prettier-ignore
|
||||
console.log("[AssistantChatPanel][history][loadHistory:SUCCESS] History loaded");
|
||||
} catch (err) {
|
||||
console.error('[AssistantChatPanel][Coherence:Failed] Failed to load history', err);
|
||||
console.error(
|
||||
"[AssistantChatPanel][history][loadHistory:FAILED] Failed to load history",
|
||||
err,
|
||||
);
|
||||
} finally {
|
||||
loadingHistory = false;
|
||||
}
|
||||
@@ -111,23 +124,30 @@
|
||||
loadingConversations = true;
|
||||
try {
|
||||
const page = reset ? 1 : conversationsPage + 1;
|
||||
const includeArchived = conversationFilter === 'archived';
|
||||
const archivedOnly = conversationFilter === 'archived';
|
||||
const includeArchived = conversationFilter === "archived";
|
||||
const archivedOnly = conversationFilter === "archived";
|
||||
const response = await getAssistantConversations(
|
||||
page,
|
||||
CONVERSATIONS_PAGE_SIZE,
|
||||
includeArchived,
|
||||
'',
|
||||
"",
|
||||
archivedOnly,
|
||||
);
|
||||
const rows = response.items || [];
|
||||
conversations = reset ? rows : [...conversations, ...rows];
|
||||
conversationsPage = page;
|
||||
conversationsHasNext = Boolean(response.has_next);
|
||||
activeConversationsTotal = response.active_total ?? activeConversationsTotal;
|
||||
archivedConversationsTotal = response.archived_total ?? archivedConversationsTotal;
|
||||
activeConversationsTotal =
|
||||
response.active_total ?? activeConversationsTotal;
|
||||
archivedConversationsTotal =
|
||||
response.archived_total ?? archivedConversationsTotal;
|
||||
// prettier-ignore
|
||||
console.log("[AssistantChatPanel][conversations][loadConversations:SUCCESS]");
|
||||
} catch (err) {
|
||||
console.error('[AssistantChatPanel][Coherence:Failed] Failed to load conversations', err);
|
||||
console.error(
|
||||
"[AssistantChatPanel][conversations][loadConversations:FAILED]",
|
||||
err,
|
||||
);
|
||||
} finally {
|
||||
loadingConversations = false;
|
||||
}
|
||||
@@ -141,11 +161,22 @@
|
||||
* @POST: Older messages are prepended while preserving order.
|
||||
*/
|
||||
async function loadOlderMessages() {
|
||||
if (loadingMoreHistory || loadingHistory || !historyHasNext || !conversationId) return;
|
||||
if (
|
||||
loadingMoreHistory ||
|
||||
loadingHistory ||
|
||||
!historyHasNext ||
|
||||
!conversationId
|
||||
)
|
||||
return;
|
||||
loadingMoreHistory = true;
|
||||
try {
|
||||
const nextPage = historyPage + 1;
|
||||
const history = await getAssistantHistory(nextPage, HISTORY_PAGE_SIZE, conversationId, true);
|
||||
const history = await getAssistantHistory(
|
||||
nextPage,
|
||||
HISTORY_PAGE_SIZE,
|
||||
conversationId,
|
||||
true,
|
||||
);
|
||||
const chunk = (history.items || []).map((msg) => ({
|
||||
...msg,
|
||||
actions: msg.actions || msg.metadata?.actions || [],
|
||||
@@ -155,8 +186,12 @@
|
||||
messages = [...uniqueChunk, ...messages];
|
||||
historyPage = nextPage;
|
||||
historyHasNext = Boolean(history.has_next);
|
||||
console.log("[AssistantChatPanel][history][loadOlderMessages:SUCCESS]");
|
||||
} catch (err) {
|
||||
console.error('[AssistantChatPanel][Coherence:Failed] Failed to load older messages', err);
|
||||
console.error(
|
||||
"[AssistantChatPanel][history][loadOlderMessages:FAILED]",
|
||||
err,
|
||||
);
|
||||
} finally {
|
||||
loadingMoreHistory = false;
|
||||
}
|
||||
@@ -170,7 +205,9 @@
|
||||
|
||||
$: if (isOpen && initialized && conversationId) {
|
||||
// Re-load only when user switched to another conversation.
|
||||
const currentFirstConversationId = messages.length ? messages[0].conversation_id : conversationId;
|
||||
const currentFirstConversationId = messages.length
|
||||
? messages[0].conversation_id
|
||||
: conversationId;
|
||||
if (currentFirstConversationId !== conversationId) {
|
||||
loadHistory();
|
||||
}
|
||||
@@ -183,11 +220,12 @@
|
||||
* @POST: user message appears at the end of messages list.
|
||||
*/
|
||||
function appendLocalUserMessage(text) {
|
||||
console.log("[AssistantChatPanel][message][appendLocalUserMessage][START]");
|
||||
messages = [
|
||||
...messages,
|
||||
{
|
||||
message_id: `local-${Date.now()}`,
|
||||
role: 'user',
|
||||
role: "user",
|
||||
text,
|
||||
created_at: new Date().toISOString(),
|
||||
},
|
||||
@@ -202,11 +240,13 @@
|
||||
* @POST: assistant message appended with state/task/actions metadata.
|
||||
*/
|
||||
function appendAssistantResponse(response) {
|
||||
// prettier-ignore
|
||||
console.log("[AssistantChatPanel][message][appendAssistantResponse][START]");
|
||||
messages = [
|
||||
...messages,
|
||||
{
|
||||
message_id: response.response_id,
|
||||
role: 'assistant',
|
||||
role: "assistant",
|
||||
text: response.text,
|
||||
state: response.state,
|
||||
task_id: response.task_id || null,
|
||||
@@ -220,12 +260,12 @@
|
||||
|
||||
function buildConversationTitle(conversation) {
|
||||
if (conversation?.title?.trim()) return conversation.title.trim();
|
||||
if (!conversation?.conversation_id) return 'Conversation';
|
||||
if (!conversation?.conversation_id) return "Conversation";
|
||||
return `Conversation ${conversation.conversation_id.slice(0, 8)}`;
|
||||
}
|
||||
|
||||
function setConversationFilter(filter) {
|
||||
if (filter !== 'active' && filter !== 'archived') return;
|
||||
if (filter !== "active" && filter !== "archived") return;
|
||||
if (conversationFilter === filter) return;
|
||||
conversationFilter = filter;
|
||||
conversations = [];
|
||||
@@ -235,9 +275,9 @@
|
||||
}
|
||||
|
||||
function formatConversationTime(iso) {
|
||||
if (!iso) return '';
|
||||
if (!iso) return "";
|
||||
const dt = new Date(iso);
|
||||
if (Number.isNaN(dt.getTime())) return '';
|
||||
if (Number.isNaN(dt.getTime())) return "";
|
||||
return dt.toLocaleString();
|
||||
}
|
||||
|
||||
@@ -249,11 +289,12 @@
|
||||
* @SIDE_EFFECT: Triggers backend command execution pipeline.
|
||||
*/
|
||||
async function handleSend() {
|
||||
console.log("[AssistantChatPanel][message][handleSend][START]");
|
||||
const text = input.trim();
|
||||
if (!text || loading) return;
|
||||
|
||||
appendLocalUserMessage(text);
|
||||
input = '';
|
||||
input = "";
|
||||
loading = true;
|
||||
|
||||
try {
|
||||
@@ -271,8 +312,8 @@
|
||||
} catch (err) {
|
||||
appendAssistantResponse({
|
||||
response_id: `error-${Date.now()}`,
|
||||
text: err.message || 'Assistant request failed',
|
||||
state: 'failed',
|
||||
text: err.message || "Assistant request failed",
|
||||
state: "failed",
|
||||
created_at: new Date().toISOString(),
|
||||
actions: [],
|
||||
});
|
||||
@@ -289,6 +330,8 @@
|
||||
* @POST: conversationId updated and history reloaded.
|
||||
*/
|
||||
async function selectConversation(conversation) {
|
||||
// prettier-ignore
|
||||
console.log("[AssistantChatPanel][conversation][selectConversation][START]");
|
||||
if (!conversation?.conversation_id) return;
|
||||
if (conversation.conversation_id === conversationId) return;
|
||||
// Invalidate any in-flight history request to avoid stale conversation overwrite.
|
||||
@@ -308,8 +351,10 @@
|
||||
* @POST: Messages reset and new conversation id bound.
|
||||
*/
|
||||
function startNewConversation() {
|
||||
// prettier-ignore
|
||||
console.log("[AssistantChatPanel][conversation][startNewConversation][START]");
|
||||
const newId =
|
||||
typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
|
||||
typeof crypto !== "undefined" && typeof crypto.randomUUID === "function"
|
||||
? crypto.randomUUID()
|
||||
: `conv-${Date.now()}`;
|
||||
setAssistantConversationId(newId);
|
||||
@@ -328,32 +373,37 @@
|
||||
* @SIDE_EFFECT: May navigate routes or call confirm/cancel API endpoints.
|
||||
*/
|
||||
async function handleAction(action, message) {
|
||||
console.log("[AssistantChatPanel][action][handleAction][START]");
|
||||
try {
|
||||
if (action.type === 'open_task' && action.target) {
|
||||
if (action.type === "open_task" && action.target) {
|
||||
openDrawerForTask(action.target);
|
||||
return;
|
||||
}
|
||||
|
||||
if (action.type === 'open_reports') {
|
||||
goto('/reports');
|
||||
if (action.type === "open_reports") {
|
||||
goto("/reports");
|
||||
return;
|
||||
}
|
||||
|
||||
if (action.type === 'confirm' && message.confirmation_id) {
|
||||
const response = await confirmAssistantOperation(message.confirmation_id);
|
||||
if (action.type === "confirm" && message.confirmation_id) {
|
||||
const response = await confirmAssistantOperation(
|
||||
message.confirmation_id,
|
||||
);
|
||||
appendAssistantResponse(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (action.type === 'cancel' && message.confirmation_id) {
|
||||
const response = await cancelAssistantOperation(message.confirmation_id);
|
||||
if (action.type === "cancel" && message.confirmation_id) {
|
||||
const response = await cancelAssistantOperation(
|
||||
message.confirmation_id,
|
||||
);
|
||||
appendAssistantResponse(response);
|
||||
}
|
||||
} catch (err) {
|
||||
appendAssistantResponse({
|
||||
response_id: `action-error-${Date.now()}`,
|
||||
text: err.message || 'Action failed',
|
||||
state: 'failed',
|
||||
text: err.message || "Action failed",
|
||||
state: "failed",
|
||||
created_at: new Date().toISOString(),
|
||||
actions: [],
|
||||
});
|
||||
@@ -368,7 +418,8 @@
|
||||
* @POST: handleSend is invoked when Enter is pressed without shift modifier.
|
||||
*/
|
||||
function handleKeydown(event) {
|
||||
if (event.key === 'Enter' && !event.shiftKey) {
|
||||
console.log("[AssistantChatPanel][input][handleKeydown][START]");
|
||||
if (event.key === "Enter" && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
handleSend();
|
||||
}
|
||||
@@ -382,12 +433,17 @@
|
||||
* @POST: Tailwind class string returned for badge rendering.
|
||||
*/
|
||||
function stateClass(state) {
|
||||
if (state === 'started') return 'bg-sky-100 text-sky-700 border-sky-200';
|
||||
if (state === 'success') return 'bg-emerald-100 text-emerald-700 border-emerald-200';
|
||||
if (state === 'needs_confirmation') return 'bg-amber-100 text-amber-700 border-amber-200';
|
||||
if (state === 'denied' || state === 'failed') return 'bg-rose-100 text-rose-700 border-rose-200';
|
||||
if (state === 'needs_clarification') return 'bg-violet-100 text-violet-700 border-violet-200';
|
||||
return 'bg-slate-100 text-slate-700 border-slate-200';
|
||||
console.log("[AssistantChatPanel][ui][stateClass][START]");
|
||||
if (state === "started") return "bg-sky-100 text-sky-700 border-sky-200";
|
||||
if (state === "success")
|
||||
return "bg-emerald-100 text-emerald-700 border-emerald-200";
|
||||
if (state === "needs_confirmation")
|
||||
return "bg-amber-100 text-amber-700 border-amber-200";
|
||||
if (state === "denied" || state === "failed")
|
||||
return "bg-rose-100 text-rose-700 border-rose-200";
|
||||
if (state === "needs_clarification")
|
||||
return "bg-violet-100 text-violet-700 border-violet-200";
|
||||
return "bg-slate-100 text-slate-700 border-slate-200";
|
||||
}
|
||||
// [/DEF:stateClass:Function]
|
||||
|
||||
@@ -398,8 +454,9 @@
|
||||
* @POST: loadOlderMessages called when boundary and more pages available.
|
||||
*/
|
||||
function handleHistoryScroll(event) {
|
||||
console.log("[AssistantChatPanel][scroll][handleHistoryScroll][START]");
|
||||
const el = event.currentTarget;
|
||||
if (!el || typeof el.scrollTop !== 'number') return;
|
||||
if (!el || typeof el.scrollTop !== "number") return;
|
||||
if (el.scrollTop <= 16) {
|
||||
loadOlderMessages();
|
||||
}
|
||||
@@ -412,18 +469,28 @@
|
||||
</script>
|
||||
|
||||
{#if isOpen}
|
||||
<div class="fixed inset-0 z-[70] bg-slate-900/30" on:click={closeAssistantChat} aria-hidden="true"></div>
|
||||
<div
|
||||
class="fixed inset-0 z-[70] bg-slate-900/30"
|
||||
on:click={closeAssistantChat}
|
||||
aria-hidden="true"
|
||||
></div>
|
||||
|
||||
<aside class="fixed right-0 top-0 z-[71] h-full w-full max-w-md border-l border-slate-200 bg-white shadow-2xl">
|
||||
<div class="flex h-14 items-center justify-between border-b border-slate-200 px-4">
|
||||
<aside
|
||||
class="fixed right-0 top-0 z-[71] h-full w-full max-w-md border-l border-slate-200 bg-white shadow-2xl"
|
||||
>
|
||||
<div
|
||||
class="flex h-14 items-center justify-between border-b border-slate-200 px-4"
|
||||
>
|
||||
<div class="flex items-center gap-2 text-slate-800">
|
||||
<Icon name="clipboard" size={18} />
|
||||
<h2 class="text-sm font-semibold">{$t.assistant?.title || 'AI Assistant'}</h2>
|
||||
<h2 class="text-sm font-semibold">
|
||||
{$t.assistant?.title || "AI Assistant"}
|
||||
</h2>
|
||||
</div>
|
||||
<button
|
||||
class="rounded-md p-1 text-slate-500 transition hover:bg-slate-100 hover:text-slate-900"
|
||||
on:click={closeAssistantChat}
|
||||
aria-label={$t.assistant?.close || 'Close assistant'}
|
||||
aria-label={$t.assistant?.close || "Close assistant"}
|
||||
>
|
||||
<Icon name="close" size={18} />
|
||||
</button>
|
||||
@@ -432,7 +499,10 @@
|
||||
<div class="flex h-[calc(100%-56px)] flex-col">
|
||||
<div class="border-b border-slate-200 px-3 py-2">
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<span class="text-xs font-semibold uppercase tracking-wide text-slate-500">Conversations</span>
|
||||
<span
|
||||
class="text-xs font-semibold uppercase tracking-wide text-slate-500"
|
||||
>Conversations</span
|
||||
>
|
||||
<button
|
||||
class="rounded-md border border-slate-300 px-2 py-1 text-[11px] font-medium text-slate-700 transition hover:bg-slate-100"
|
||||
on:click={startNewConversation}
|
||||
@@ -442,14 +512,20 @@
|
||||
</div>
|
||||
<div class="mb-2 flex items-center gap-1">
|
||||
<button
|
||||
class="rounded-md border px-2 py-1 text-[11px] font-medium transition {conversationFilter === 'active' ? 'border-sky-300 bg-sky-50 text-sky-900' : 'border-slate-300 bg-white text-slate-700 hover:bg-slate-100'}"
|
||||
on:click={() => setConversationFilter('active')}
|
||||
class="rounded-md border px-2 py-1 text-[11px] font-medium transition {conversationFilter ===
|
||||
'active'
|
||||
? 'border-sky-300 bg-sky-50 text-sky-900'
|
||||
: 'border-slate-300 bg-white text-slate-700 hover:bg-slate-100'}"
|
||||
on:click={() => setConversationFilter("active")}
|
||||
>
|
||||
Active ({activeConversationsTotal})
|
||||
</button>
|
||||
<button
|
||||
class="rounded-md border px-2 py-1 text-[11px] font-medium transition {conversationFilter === 'archived' ? 'border-sky-300 bg-sky-50 text-sky-900' : 'border-slate-300 bg-white text-slate-700 hover:bg-slate-100'}"
|
||||
on:click={() => setConversationFilter('archived')}
|
||||
class="rounded-md border px-2 py-1 text-[11px] font-medium transition {conversationFilter ===
|
||||
'archived'
|
||||
? 'border-sky-300 bg-sky-50 text-sky-900'
|
||||
: 'border-slate-300 bg-white text-slate-700 hover:bg-slate-100'}"
|
||||
on:click={() => setConversationFilter("archived")}
|
||||
>
|
||||
Archived ({archivedConversationsTotal})
|
||||
</button>
|
||||
@@ -457,16 +533,27 @@
|
||||
<div class="flex gap-2 overflow-x-auto pb-1">
|
||||
{#each conversations as convo (convo.conversation_id)}
|
||||
<button
|
||||
class="min-w-[140px] max-w-[220px] rounded-lg border px-2.5 py-1.5 text-left text-xs transition {convo.conversation_id === conversationId ? 'border-sky-300 bg-sky-50 text-sky-900' : 'border-slate-200 bg-white text-slate-700 hover:bg-slate-50'}"
|
||||
class="min-w-[140px] max-w-[220px] rounded-lg border px-2.5 py-1.5 text-left text-xs transition {convo.conversation_id ===
|
||||
conversationId
|
||||
? 'border-sky-300 bg-sky-50 text-sky-900'
|
||||
: 'border-slate-200 bg-white text-slate-700 hover:bg-slate-50'}"
|
||||
on:click={() => selectConversation(convo)}
|
||||
title={formatConversationTime(convo.updated_at)}
|
||||
>
|
||||
<div class="truncate font-semibold">{buildConversationTitle(convo)}</div>
|
||||
<div class="truncate text-[10px] text-slate-500">{convo.last_message || ''}</div>
|
||||
<div class="truncate font-semibold">
|
||||
{buildConversationTitle(convo)}
|
||||
</div>
|
||||
<div class="truncate text-[10px] text-slate-500">
|
||||
{convo.last_message || ""}
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
{#if loadingConversations}
|
||||
<div class="rounded-lg border border-slate-200 px-2.5 py-1.5 text-xs text-slate-500">...</div>
|
||||
<div
|
||||
class="rounded-lg border border-slate-200 px-2.5 py-1.5 text-xs text-slate-500"
|
||||
>
|
||||
...
|
||||
</div>
|
||||
{/if}
|
||||
{#if conversationsHasNext}
|
||||
<button
|
||||
@@ -479,15 +566,29 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 space-y-3 overflow-y-auto p-4" bind:this={historyViewport} on:scroll={handleHistoryScroll}>
|
||||
<div
|
||||
class="flex-1 space-y-3 overflow-y-auto p-4"
|
||||
bind:this={historyViewport}
|
||||
on:scroll={handleHistoryScroll}
|
||||
>
|
||||
{#if loadingMoreHistory}
|
||||
<div class="rounded-lg border border-slate-200 bg-slate-50 p-2 text-center text-xs text-slate-500">Loading older messages...</div>
|
||||
<div
|
||||
class="rounded-lg border border-slate-200 bg-slate-50 p-2 text-center text-xs text-slate-500"
|
||||
>
|
||||
Loading older messages...
|
||||
</div>
|
||||
{/if}
|
||||
{#if loadingHistory}
|
||||
<div class="rounded-lg border border-slate-200 bg-slate-50 p-3 text-sm text-slate-600">{$t.assistant?.loading_history || 'Loading history...'}</div>
|
||||
<div
|
||||
class="rounded-lg border border-slate-200 bg-slate-50 p-3 text-sm text-slate-600"
|
||||
>
|
||||
{$t.assistant?.loading_history || "Loading history..."}
|
||||
</div>
|
||||
{:else if messages.length === 0}
|
||||
<div class="rounded-lg border border-slate-200 bg-slate-50 p-3 text-sm text-slate-600">
|
||||
{$t.assistant?.try_commands || 'Try commands:'}
|
||||
<div
|
||||
class="rounded-lg border border-slate-200 bg-slate-50 p-3 text-sm text-slate-600"
|
||||
>
|
||||
{$t.assistant?.try_commands || "Try commands:"}
|
||||
<div class="mt-2 space-y-1 text-xs">
|
||||
<div>• сделай ветку feature/new-dashboard для дашборда 42</div>
|
||||
<div>• запусти миграцию с dev на prod для дашборда 42</div>
|
||||
@@ -497,29 +598,44 @@
|
||||
{/if}
|
||||
|
||||
{#each messages as message (message.message_id)}
|
||||
<div class={message.role === 'user' ? 'ml-8' : 'mr-8'}>
|
||||
<div class="rounded-xl border p-3 {message.role === 'user' ? 'border-sky-200 bg-sky-50' : 'border-slate-200 bg-white'}">
|
||||
<div class={message.role === "user" ? "ml-8" : "mr-8"}>
|
||||
<div
|
||||
class="rounded-xl border p-3 {message.role === 'user'
|
||||
? 'border-sky-200 bg-sky-50'
|
||||
: 'border-slate-200 bg-white'}"
|
||||
>
|
||||
<div class="mb-1 flex items-center justify-between gap-2">
|
||||
<span class="text-[11px] font-semibold uppercase tracking-wide text-slate-500">
|
||||
{message.role === 'user' ? 'You' : 'Assistant'}
|
||||
<span
|
||||
class="text-[11px] font-semibold uppercase tracking-wide text-slate-500"
|
||||
>
|
||||
{message.role === "user" ? "You" : "Assistant"}
|
||||
</span>
|
||||
{#if message.state}
|
||||
<span class="rounded-md border px-2 py-0.5 text-[10px] font-medium {stateClass(message.state)}">
|
||||
<span
|
||||
class="rounded-md border px-2 py-0.5 text-[10px] font-medium {stateClass(
|
||||
message.state,
|
||||
)}"
|
||||
>
|
||||
{$t.assistant?.states?.[message.state] || message.state}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="whitespace-pre-wrap text-sm text-slate-800">{message.text}</div>
|
||||
<div class="whitespace-pre-wrap text-sm text-slate-800">
|
||||
{message.text}
|
||||
</div>
|
||||
|
||||
{#if message.task_id}
|
||||
<div class="mt-2 flex items-center gap-2">
|
||||
<span class="rounded border border-slate-200 bg-slate-50 px-2 py-0.5 text-xs text-slate-700">task_id: {message.task_id}</span>
|
||||
<span
|
||||
class="rounded border border-slate-200 bg-slate-50 px-2 py-0.5 text-xs text-slate-700"
|
||||
>task_id: {message.task_id}</span
|
||||
>
|
||||
<button
|
||||
class="text-xs font-medium text-sky-700 hover:text-sky-900"
|
||||
on:click={() => openDrawerForTask(message.task_id)}
|
||||
>
|
||||
{$t.assistant?.open_task_drawer || 'Open Task Drawer'}
|
||||
{$t.assistant?.open_task_drawer || "Open Task Drawer"}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -544,12 +660,14 @@
|
||||
<div class="mr-8">
|
||||
<div class="rounded-xl border border-slate-200 bg-white p-3">
|
||||
<div class="mb-1 flex items-center justify-between gap-2">
|
||||
<span class="text-[11px] font-semibold uppercase tracking-wide text-slate-500">
|
||||
<span
|
||||
class="text-[11px] font-semibold uppercase tracking-wide text-slate-500"
|
||||
>
|
||||
Assistant
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-sm text-slate-700">
|
||||
<span>{$t.assistant?.thinking || 'Думаю'}</span>
|
||||
<span>{$t.assistant?.thinking || "Думаю"}</span>
|
||||
<span class="thinking-dots" aria-hidden="true">
|
||||
<span></span><span></span><span></span>
|
||||
</span>
|
||||
@@ -564,7 +682,7 @@
|
||||
<textarea
|
||||
bind:value={input}
|
||||
rows="2"
|
||||
placeholder={$t.assistant?.input_placeholder || 'Type a command...'}
|
||||
placeholder={$t.assistant?.input_placeholder || "Type a command..."}
|
||||
class="min-h-[52px] w-full resize-y rounded-lg border border-slate-300 px-3 py-2 text-sm outline-none transition focus:border-sky-400 focus:ring-2 focus:ring-sky-100"
|
||||
on:keydown={handleKeydown}
|
||||
></textarea>
|
||||
@@ -573,7 +691,7 @@
|
||||
on:click={handleSend}
|
||||
disabled={loading || !input.trim()}
|
||||
>
|
||||
{loading ? '...' : ($t.assistant?.send || 'Send')}
|
||||
{loading ? "..." : $t.assistant?.send || "Send"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -581,6 +699,8 @@
|
||||
</aside>
|
||||
{/if}
|
||||
|
||||
<!-- [/DEF:AssistantChatPanel:Component] -->
|
||||
|
||||
<style>
|
||||
.thinking-dots {
|
||||
display: inline-flex;
|
||||
@@ -618,4 +738,3 @@
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<!-- [/DEF:AssistantChatPanel:Component] -->
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
|
||||
// Close drawer
|
||||
function handleClose() {
|
||||
console.log("[TaskDrawer][Action] Close drawer");
|
||||
console.log("[TaskDrawer][ui][Close_drawer]");
|
||||
closeDrawer();
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
console.log("[TaskDrawer][WebSocket] Received message:", data);
|
||||
console.log(`[TaskDrawer][WebSocket][Message_Received] ${data.message}`);
|
||||
|
||||
realTimeLogs = [...realTimeLogs, data];
|
||||
|
||||
@@ -118,28 +118,40 @@
|
||||
};
|
||||
}
|
||||
|
||||
// Disconnect WebSocket
|
||||
// [DEF:disconnectWebSocket:Function]
|
||||
/**
|
||||
* @PURPOSE: Disconnects the active WebSocket connection
|
||||
* @PRE: ws may or may not be initialized
|
||||
* @POST: ws is closed and set to null
|
||||
* @TIER: STANDARD
|
||||
*/
|
||||
function disconnectWebSocket() {
|
||||
console.log("[TaskDrawer][WebSocket][disconnectWebSocket:START]");
|
||||
if (ws) {
|
||||
ws.close();
|
||||
ws = null;
|
||||
}
|
||||
}
|
||||
// [/DEF:disconnectWebSocket:Function]
|
||||
|
||||
// [DEF:loadRecentTasks:Function]
|
||||
/**
|
||||
* @PURPOSE: Load recent tasks for list mode display
|
||||
* @PRE: User is on task drawer or api is ready.
|
||||
* @POST: recentTasks array populated with task list
|
||||
*/
|
||||
async function loadRecentTasks() {
|
||||
loadingTasks = true;
|
||||
try {
|
||||
console.log("[TaskDrawer][API][loadRecentTasks:STARTED]");
|
||||
// API returns List[Task] directly, not {tasks: [...]}
|
||||
const response = await api.getTasks();
|
||||
recentTasks = Array.isArray(response) ? response : (response.tasks || []);
|
||||
console.log("[TaskDrawer][Action] Loaded recent tasks:", recentTasks.length);
|
||||
recentTasks = Array.isArray(response) ? response : response.tasks || [];
|
||||
console.log(
|
||||
`[TaskDrawer][API][loadRecentTasks:SUCCESS] loaded ${recentTasks.length} tasks`,
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("[TaskDrawer][Coherence:Failed] Failed to load tasks:", err);
|
||||
console.error("[TaskDrawer][API][loadRecentTasks:FAILED]", err);
|
||||
recentTasks = [];
|
||||
} finally {
|
||||
loadingTasks = false;
|
||||
@@ -150,11 +162,14 @@
|
||||
// [DEF:selectTask:Function]
|
||||
/**
|
||||
* @PURPOSE: Select a task from list to view details
|
||||
* @PRE: task is a valid task object
|
||||
* @POST: drawer state updated to show task details
|
||||
*/
|
||||
function selectTask(task) {
|
||||
taskDrawerStore.update(state => ({
|
||||
console.log("[TaskDrawer][UI][selectTask:START]");
|
||||
taskDrawerStore.update((state) => ({
|
||||
...state,
|
||||
activeTaskId: task.id
|
||||
activeTaskId: task.id,
|
||||
}));
|
||||
}
|
||||
// [/DEF:selectTask:Function]
|
||||
@@ -162,14 +177,19 @@
|
||||
// [DEF:goBackToList:Function]
|
||||
/**
|
||||
* @PURPOSE: Return to task list view from task details
|
||||
* @PRE: Drawer is open and activeTaskId is set
|
||||
* @POST: Drawer switches to list view and reloads tasks
|
||||
* @TIER: STANDARD
|
||||
*/
|
||||
function goBackToList() {
|
||||
taskDrawerStore.update(state => ({
|
||||
console.log("[TaskDrawer][UI][goBackToList:START]");
|
||||
taskDrawerStore.update((state) => ({
|
||||
...state,
|
||||
activeTaskId: null
|
||||
activeTaskId: null,
|
||||
}));
|
||||
// Reload the task list
|
||||
loadRecentTasks();
|
||||
console.log("[TaskDrawer][UI][goBackToList:SUCCESS]");
|
||||
}
|
||||
// [/DEF:goBackToList:Function]
|
||||
|
||||
@@ -202,100 +222,145 @@
|
||||
aria-modal="false"
|
||||
aria-label={$t.tasks?.drawer || "Task drawer"}
|
||||
>
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between border-b border-slate-200 bg-white px-5 py-3.5">
|
||||
<div class="flex items-center gap-2.5">
|
||||
{#if !activeTaskId && recentTasks.length > 0}
|
||||
<span class="flex items-center justify-center p-1.5 mr-1 text-cyan-400">
|
||||
<Icon name="list" size={16} strokeWidth={2} />
|
||||
</span>
|
||||
{:else if activeTaskId}
|
||||
<button
|
||||
class="flex items-center justify-center p-1.5 rounded-md text-slate-500 bg-transparent border-none cursor-pointer transition-all hover:text-slate-100 hover:bg-slate-800"
|
||||
on:click={goBackToList}
|
||||
aria-label={$t.tasks?.back_to_list || "Back to task list"}
|
||||
>
|
||||
<Icon name="back" size={16} strokeWidth={2} />
|
||||
</button>
|
||||
{/if}
|
||||
<h2 class="text-sm font-semibold tracking-tight text-slate-900">
|
||||
{activeTaskId ? ($t.tasks?.details_logs || 'Task Details & Logs') : ($t.tasks?.recent || 'Recent Tasks')}
|
||||
</h2>
|
||||
{#if shortTaskId}
|
||||
<span class="text-xs font-mono text-slate-500 bg-slate-800 px-2 py-0.5 rounded">{shortTaskId}…</span>
|
||||
{/if}
|
||||
{#if taskStatus}
|
||||
<span class="text-xs font-semibold uppercase tracking-wider px-2 py-0.5 rounded-full {taskStatus.toLowerCase() === 'running' ? 'text-cyan-400 bg-cyan-400/10 border border-cyan-400/20' : taskStatus.toLowerCase() === 'success' ? 'text-green-400 bg-green-400/10 border border-green-400/20' : 'text-red-400 bg-red-400/10 border border-red-400/20'}"
|
||||
>{taskStatus}</span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
class="rounded-md border border-slate-300 bg-slate-50 px-2.5 py-1 text-xs font-semibold text-slate-700 transition-colors hover:bg-slate-100"
|
||||
on:click={goToReportsPage}
|
||||
<!-- Header -->
|
||||
<div
|
||||
class="flex items-center justify-between border-b border-slate-200 bg-white px-5 py-3.5"
|
||||
>
|
||||
<div class="flex items-center gap-2.5">
|
||||
{#if !activeTaskId && recentTasks.length > 0}
|
||||
<span
|
||||
class="flex items-center justify-center p-1.5 mr-1 text-cyan-400"
|
||||
>
|
||||
{$t.nav?.reports || "Reports"}
|
||||
</button>
|
||||
<Icon name="list" size={16} strokeWidth={2} />
|
||||
</span>
|
||||
{:else if activeTaskId}
|
||||
<button
|
||||
class="p-1.5 rounded-md text-slate-500 bg-transparent border-none cursor-pointer transition-all hover:text-slate-100 hover:bg-slate-800"
|
||||
on:click={handleClose}
|
||||
aria-label={$t.tasks?.close_drawer || "Close drawer"}
|
||||
class="flex items-center justify-center p-1.5 rounded-md text-slate-500 bg-transparent border-none cursor-pointer transition-all hover:text-slate-100 hover:bg-slate-800"
|
||||
on:click={goBackToList}
|
||||
aria-label={$t.tasks?.back_to_list || "Back to task list"}
|
||||
>
|
||||
<Icon name="close" size={18} strokeWidth={2} />
|
||||
<Icon name="back" size={16} strokeWidth={2} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="flex-1 overflow-hidden flex flex-col">
|
||||
{#if activeTaskId}
|
||||
<TaskLogViewer
|
||||
inline={true}
|
||||
taskId={activeTaskId}
|
||||
{taskStatus}
|
||||
{realTimeLogs}
|
||||
/>
|
||||
{:else if loadingTasks}
|
||||
<div class="flex flex-col items-center justify-center p-12 text-slate-500">
|
||||
<div class="mb-4 h-8 w-8 animate-spin rounded-full border-2 border-slate-200 border-t-blue-500"></div>
|
||||
<p>{$t.tasks?.loading || 'Loading tasks...'}</p>
|
||||
</div>
|
||||
{:else if recentTasks.length > 0}
|
||||
<div class="p-4">
|
||||
<h3 class="text-sm font-semibold text-slate-100 mb-4 pb-2 border-b border-slate-800">{$t.tasks?.recent || 'Recent Tasks'}</h3>
|
||||
{#each recentTasks as task}
|
||||
<button
|
||||
class="flex items-center gap-3 w-full p-3 mb-2 bg-slate-800 border border-slate-700 rounded-lg cursor-pointer transition-all hover:bg-slate-700 hover:border-slate-600 text-left"
|
||||
on:click={() => selectTask(task)}
|
||||
>
|
||||
<span class="font-mono text-xs text-slate-500">{task.id?.substring(0, 8) || ($t.common?.not_available || 'N/A')}...</span>
|
||||
<span class="flex-1 text-sm text-slate-100 font-medium">{task.plugin_id || ($t.common?.unknown || 'Unknown')}</span>
|
||||
<span class="text-xs font-semibold uppercase px-2 py-1 rounded-full {task.status?.toLowerCase() === 'running' || task.status?.toLowerCase() === 'pending' ? 'bg-cyan-500/15 text-cyan-400' : task.status?.toLowerCase() === 'completed' || task.status?.toLowerCase() === 'success' ? 'bg-green-500/15 text-green-400' : task.status?.toLowerCase() === 'failed' || task.status?.toLowerCase() === 'error' ? 'bg-red-500/15 text-red-400' : 'bg-slate-500/15 text-slate-400'}">{task.status || ($t.common?.unknown || 'UNKNOWN')}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-col items-center justify-center h-full text-slate-500">
|
||||
<Icon
|
||||
name="clipboard"
|
||||
size={48}
|
||||
strokeWidth={1.6}
|
||||
className="mb-3 text-slate-700"
|
||||
/>
|
||||
<p>{$t.tasks?.select_task || 'No recent tasks'}</p>
|
||||
</div>
|
||||
{/if}
|
||||
<h2 class="text-sm font-semibold tracking-tight text-slate-900">
|
||||
{activeTaskId
|
||||
? $t.tasks?.details_logs || "Task Details & Logs"
|
||||
: $t.tasks?.recent || "Recent Tasks"}
|
||||
</h2>
|
||||
{#if shortTaskId}
|
||||
<span
|
||||
class="text-xs font-mono text-slate-500 bg-slate-800 px-2 py-0.5 rounded"
|
||||
>{shortTaskId}…</span
|
||||
>
|
||||
{/if}
|
||||
{#if taskStatus}
|
||||
<span
|
||||
class="text-xs font-semibold uppercase tracking-wider px-2 py-0.5 rounded-full {taskStatus.toLowerCase() ===
|
||||
'running'
|
||||
? 'text-cyan-400 bg-cyan-400/10 border border-cyan-400/20'
|
||||
: taskStatus.toLowerCase() === 'success'
|
||||
? 'text-green-400 bg-green-400/10 border border-green-400/20'
|
||||
: 'text-red-400 bg-red-400/10 border border-red-400/20'}"
|
||||
>{taskStatus}</span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="flex items-center gap-2 justify-center px-4 py-2.5 border-t border-slate-800 bg-slate-900">
|
||||
<div class="w-1.5 h-1.5 rounded-full bg-cyan-400 animate-pulse"></div>
|
||||
<p class="text-xs text-slate-500">
|
||||
{$t.tasks?.footer_text || 'Task continues running in background'}
|
||||
</p>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
class="rounded-md border border-slate-300 bg-slate-50 px-2.5 py-1 text-xs font-semibold text-slate-700 transition-colors hover:bg-slate-100"
|
||||
on:click={goToReportsPage}
|
||||
>
|
||||
{$t.nav?.reports || "Reports"}
|
||||
</button>
|
||||
<button
|
||||
class="p-1.5 rounded-md text-slate-500 bg-transparent border-none cursor-pointer transition-all hover:text-slate-100 hover:bg-slate-800"
|
||||
on:click={handleClose}
|
||||
aria-label={$t.tasks?.close_drawer || "Close drawer"}
|
||||
>
|
||||
<Icon name="close" size={18} strokeWidth={2} />
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- [/DEF:TaskDrawer:Component] -->
|
||||
<!-- Content -->
|
||||
<div class="flex-1 overflow-hidden flex flex-col">
|
||||
{#if activeTaskId}
|
||||
<TaskLogViewer
|
||||
inline={true}
|
||||
taskId={activeTaskId}
|
||||
{taskStatus}
|
||||
{realTimeLogs}
|
||||
/>
|
||||
{:else if loadingTasks}
|
||||
<div
|
||||
class="flex flex-col items-center justify-center p-12 text-slate-500"
|
||||
>
|
||||
<div
|
||||
class="mb-4 h-8 w-8 animate-spin rounded-full border-2 border-slate-200 border-t-blue-500"
|
||||
></div>
|
||||
<p>{$t.tasks?.loading || "Loading tasks..."}</p>
|
||||
</div>
|
||||
{:else if recentTasks.length > 0}
|
||||
<div class="p-4">
|
||||
<h3
|
||||
class="text-sm font-semibold text-slate-100 mb-4 pb-2 border-b border-slate-800"
|
||||
>
|
||||
{$t.tasks?.recent || "Recent Tasks"}
|
||||
</h3>
|
||||
{#each recentTasks as task}
|
||||
<button
|
||||
class="flex items-center gap-3 w-full p-3 mb-2 bg-slate-800 border border-slate-700 rounded-lg cursor-pointer transition-all hover:bg-slate-700 hover:border-slate-600 text-left"
|
||||
on:click={() => selectTask(task)}
|
||||
>
|
||||
<span class="font-mono text-xs text-slate-500"
|
||||
>{task.id?.substring(0, 8) ||
|
||||
$t.common?.not_available ||
|
||||
"N/A"}...</span
|
||||
>
|
||||
<span class="flex-1 text-sm text-slate-100 font-medium"
|
||||
>{task.plugin_id || $t.common?.unknown || "Unknown"}</span
|
||||
>
|
||||
<span
|
||||
class="text-xs font-semibold uppercase px-2 py-1 rounded-full {task.status?.toLowerCase() ===
|
||||
'running' || task.status?.toLowerCase() === 'pending'
|
||||
? 'bg-cyan-500/15 text-cyan-400'
|
||||
: task.status?.toLowerCase() === 'completed' ||
|
||||
task.status?.toLowerCase() === 'success'
|
||||
? 'bg-green-500/15 text-green-400'
|
||||
: task.status?.toLowerCase() === 'failed' ||
|
||||
task.status?.toLowerCase() === 'error'
|
||||
? 'bg-red-500/15 text-red-400'
|
||||
: 'bg-slate-500/15 text-slate-400'}"
|
||||
>{task.status || $t.common?.unknown || "UNKNOWN"}</span
|
||||
>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
class="flex flex-col items-center justify-center h-full text-slate-500"
|
||||
>
|
||||
<Icon
|
||||
name="clipboard"
|
||||
size={48}
|
||||
strokeWidth={1.6}
|
||||
className="mb-3 text-slate-700"
|
||||
/>
|
||||
<p>{$t.tasks?.select_task || "No recent tasks"}</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div
|
||||
class="flex items-center gap-2 justify-center px-4 py-2.5 border-t border-slate-800 bg-slate-900"
|
||||
>
|
||||
<div class="w-1.5 h-1.5 rounded-full bg-cyan-400 animate-pulse"></div>
|
||||
<p class="text-xs text-slate-500">
|
||||
{$t.tasks?.footer_text || "Task continues running in background"}
|
||||
</p>
|
||||
</div>
|
||||
</aside>
|
||||
<!-- [/DEF:TaskDrawer:Component] -->
|
||||
{/if}
|
||||
```
|
||||
|
||||
@@ -52,6 +52,8 @@ export const REPORT_TYPE_PROFILES = {
|
||||
// @POST: Returns one profile object.
|
||||
export function getReportTypeProfile(taskType) {
|
||||
const key = typeof taskType === 'string' ? taskType : 'unknown';
|
||||
console.log("[reports][ui][getReportTypeProfile][STATE:START]");
|
||||
console.log("[reports][ui][getReportTypeProfile] Resolved type '" + taskType + "' to profile '" + key + "'");
|
||||
return REPORT_TYPE_PROFILES[key] || REPORT_TYPE_PROFILES.unknown;
|
||||
}
|
||||
// [/DEF:getReportTypeProfile:Function]
|
||||
|
||||
Reference in New Issue
Block a user