tasks 033 updated

This commit is contained in:
2026-06-30 19:05:17 +03:00
parent f1810090b6
commit e174c11d4a
23 changed files with 1976 additions and 213 deletions

View File

@@ -32,7 +32,6 @@
// ── Local state ─────────────────────────────────────────────────
let inputText = $state("");
let messagesContainer: HTMLDivElement | undefined = $state();
let fileInputRef: HTMLInputElement | undefined = $state();
let pendingFile: File | null = $state(null);
let isDragOver = $state(false);
let dragCounter = 0;
@@ -207,7 +206,8 @@
function removeFile() {
pendingFile = null;
if (fileInputRef) fileInputRef.value = "";
const input = document.getElementById("agent-chat-file") as HTMLInputElement | null;
if (input) input.value = "";
}
// ── Drag & drop ────────────────────────────────────────────────
@@ -320,9 +320,12 @@
<h1 class="truncate text-sm font-semibold text-text">
{model.currentConversationTitle}
</h1>
<span class="hidden rounded-md border border-border bg-surface-page px-2 py-0.5 font-mono text-[11px] text-text-muted sm:inline">
<span class="rounded-md border border-border bg-surface-page px-2 py-0.5 font-mono text-[11px] text-text-muted">
conv:{model.conversationIdLabel.slice(0, 8)}
</span>
<span class={`inline-flex items-center gap-1 rounded-md border px-1.5 py-0.5 text-[11px] font-medium ${phaseClasses(model.agentPhaseTone)}`}>
{$t.assistant?.phases?.[model.agentPhase] || model.agentPhase}
</span>
</div>
{#if model.connectionState === "disconnected"}
<button
@@ -386,13 +389,6 @@
</div>
<div class="mt-3 flex flex-wrap items-center gap-2 text-xs">
<span class={`inline-flex items-center gap-1.5 rounded-md border px-2 py-1 font-medium ${phaseClasses(model.agentPhaseTone)}`}>
<ConnectionIndicator
color={model.connectionDotColor as "success" | "warning" | "destructive"}
title={model.connectionState === "connected" ? "Agent connected" : "Agent disconnected"}
/>
{$t.assistant?.phases?.[model.agentPhase] || model.agentPhase}
</span>
{#if model.envIdLabel !== "—"}
<span class="inline-flex items-center gap-1.5 rounded-md border border-border bg-surface-page px-2 py-1 text-text-muted">
<Icon name="database" size={13} />
@@ -731,24 +727,23 @@
}
}}
></textarea>
<button
class="absolute right-2 bottom-2 rounded-lg p-1.5 text-text-subtle transition hover:text-text-muted disabled:opacity-40"
onclick={() => fileInputRef?.click()}
disabled={isInputLocked}
<label
for="agent-chat-file"
class="absolute right-2 bottom-2 cursor-pointer rounded-lg p-1.5 text-text-subtle transition hover:text-text-muted aria-disabled:opacity-40 aria-disabled:pointer-events-none"
aria-disabled={isInputLocked}
aria-label="Прикрепить файл"
title="Прикрепить файл"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13"/>
</svg>
</button>
</label>
<input
id="agent-chat-file"
name="agent_chat_file"
type="file"
accept=".pdf,.xlsx,.json,.csv,.txt,.png,.jpeg,.jpg"
class="hidden"
bind:this={fileInputRef}
onchange={handleFileSelect}
/>
</div>

View File

@@ -104,6 +104,7 @@ export class AgentChatModel {
private _submission: ReturnType<GradioClient["submit"]> | null = null;
private _conversationsPage: number = 1;
private _conversationsHasNext: boolean = $state(false);
private _isLoadingConversations: boolean = false;
private _historyPage: number = 1;
private _historyHasNext: boolean = $state(false);
private _messageQueue: Array<{ text: string; files?: File[] }> = $state([]);
@@ -550,41 +551,54 @@ export class AgentChatModel {
// ── Actions — P2 (data + lifecycle) ────────────────────────────
async loadConversations(reset: boolean = false): Promise<void> {
// Guard against concurrent calls — duplicate keys in the sidebar
// each block would crash Svelte with each_key_duplicate.
if (this._isLoadingConversations) {
log("AgentChat.Model", "REFLECT", "Skipping loadConversations (already in progress)");
return;
}
this._isLoadingConversations = true;
log("AgentChat.Model", "REASON", "Loading conversations", { reset });
try {
if (reset) this._conversationsPage = 1;
const res = await getAssistantConversations(this._conversationsPage, 20, false, "");
const items = res.items || [];
this.conversations = reset
? items.map((item: Record<string, unknown>) => ({
id: item.conversation_id ?? item.id ?? "",
title: item.title ?? "",
updated_at: item.updated_at ?? "",
message_count: item.message_count ?? 0,
last_role: item.last_role ?? null,
has_tool_calls: item.has_tool_calls ?? false,
has_error: item.has_error ?? false,
risk_level: item.risk_level ?? null,
}))
: [...this.conversations, ...items.map((item: Record<string, unknown>) => ({
id: item.conversation_id ?? item.id ?? "",
title: item.title ?? "",
updated_at: item.updated_at ?? "",
message_count: item.message_count ?? 0,
last_role: item.last_role ?? null,
has_tool_calls: item.has_tool_calls ?? false,
has_error: item.has_error ?? false,
risk_level: item.risk_level ?? null,
}))];
const mapped = items.map((item: Record<string, unknown>) => ({
id: item.conversation_id ?? item.id ?? "",
title: item.title ?? "",
updated_at: item.updated_at ?? "",
message_count: item.message_count ?? 0,
last_role: item.last_role ?? null,
has_tool_calls: item.has_tool_calls ?? false,
has_error: item.has_error ?? false,
risk_level: item.risk_level ?? null,
}));
if (reset) {
this.conversations = mapped;
} else {
// Defensive dedup: skip items already present (prevents duplicates
// from overlapping API pages or stale concurrent calls).
const existingIds = new Set(this.conversations.map(c => c.id));
const newItems = mapped.filter(item => !existingIds.has(item.id));
this.conversations = [...this.conversations, ...newItems];
}
this._conversationsHasNext = Boolean(res.has_next);
this._conversationsPage++;
} catch (e: unknown) {
this.error = e instanceof Error ? e.message : "Failed to load conversations";
log("AgentChat.Model", "EXPLORE", "Load conversations failed", {}, this.error);
} finally {
this._isLoadingConversations = false;
}
}
async searchConversations(query: string): Promise<void> {
// Guard against concurrent loads; shares _conversationsPage state.
if (this._isLoadingConversations) {
log("AgentChat.Model", "REFLECT", "Skipping searchConversations (load already in progress)");
return;
}
this._isLoadingConversations = true;
log("AgentChat.Model", "REASON", "Searching conversations", { query });
try {
this._conversationsPage = 1;
@@ -604,6 +618,8 @@ export class AgentChatModel {
} catch (e: unknown) {
this.error = e instanceof Error ? e.message : "Failed to search conversations";
log("AgentChat.Model", "EXPLORE", "Search conversations failed", {}, this.error);
} finally {
this._isLoadingConversations = false;
}
}