tasks 033 updated
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user