Files
OllaHub/components/chat/ConversationSidebar.tsx
T
Evandro Fonseca Junior a8bb7cc22d feat(ollama): enhance model pull with improved error handling, persistence, and UI
- Add catalog metadata lookup for better error messages with hints (e.g., for missing manifests or tags)
- Persist downloaded model references in saved remote models for future reuse
- Replace searchRemoteModels import with getSavedRemoteModels in search route
- Enhance pull progress UI to display download bytes, digest, and fallback message when data is unavailable
- Improve error payload with model details, URL, and tags count to guide users toward valid tags or models
2025-10-29 19:42:24 -03:00

177 lines
6.1 KiB
TypeScript

"use client";
import { useState, useEffect } from "react";
import { MessageSquare, Plus, Trash2 } from "lucide-react";
type Conversation = {
id: string;
title: string;
model: string;
updated_at: number;
};
type ConversationSidebarProps = {
currentConversationId: string | null;
onSelectConversation: (id: string) => void;
onNewConversation: () => void;
refreshTrigger?: number; // Para forçar atualização
};
export function ConversationSidebar({
currentConversationId,
onSelectConversation,
onNewConversation,
refreshTrigger,
}: ConversationSidebarProps) {
const [conversations, setConversations] = useState<Conversation[]>([]);
const [loading, setLoading] = useState(true);
const [creating, setCreating] = useState(false);
const [query, setQuery] = useState("");
useEffect(() => {
loadConversations();
}, []);
// Recarregar conversas quando refreshTrigger mudar
useEffect(() => {
if (refreshTrigger !== undefined) {
loadConversations();
}
}, [refreshTrigger]);
const loadConversations = async () => {
try {
const response = await fetch("/api/conversations");
const data = await response.json();
setConversations(data);
} catch (error) {
console.error("Erro ao carregar conversas:", error);
} finally {
setLoading(false);
}
};
const handleDelete = async (id: string, e: React.MouseEvent) => {
e.stopPropagation();
if (!confirm("Deletar esta conversa?")) return;
try {
await fetch(`/api/conversations/${id}`, { method: "DELETE" });
setConversations((prev) => prev.filter((c) => c.id !== id));
if (currentConversationId === id) {
onNewConversation();
}
} catch (error) {
console.error("Erro ao deletar conversa:", error);
}
};
const filteredConversations = conversations.filter((c) =>
c.title.toLowerCase().includes(query.toLowerCase())
);
return (
<div className="w-64 lg:w-72 h-full bg-[var(--surface)] border-r border-[var(--border)] flex flex-col">
{/* Header */}
<div className="p-4 border-b border-[var(--border)] sticky top-0 z-10 bg-[color-mix(in_oklab,var(--surface),black_2%)]/90 backdrop-blur supports-[backdrop-filter]:bg-[color-mix(in_oklab,var(--surface),black_2%)]/70">
<button
onClick={async () => {
if (creating) return;
setCreating(true);
try {
// Buscar modelo selecionado nas preferências
const prefsRes = await fetch("/api/preferences");
const prefs = await prefsRes.json();
const model = prefs?.selectedModel;
if (!model) {
alert("Selecione um modelo antes de criar uma conversa.");
return;
}
// Criar conversa vazia (título padrão gerado no backend)
const res = await fetch("/api/conversations", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ model }),
});
if (!res.ok) throw new Error("Falha ao criar conversa");
const { id, title } = await res.json();
// Inserir no topo da lista e selecionar
const newConv: Conversation = {
id,
title,
model,
updated_at: Date.now(),
};
setConversations((prev) => [newConv, ...prev]);
onSelectConversation(id);
// Notificar container para possíveis efeitos colaterais
onNewConversation();
} catch (e) {
console.error(e);
alert("Não foi possível criar a conversa.");
} finally {
setCreating(false);
}
}}
disabled={creating}
className="w-full flex items-center justify-center gap-2 px-4 py-2 bg-[var(--accent)] text-white rounded-lg hover:opacity-90 transition-opacity disabled:opacity-60"
>
<Plus className="h-4 w-4" />
{creating ? "Criando..." : "Nova Conversa"}
</button>
<div className="mt-3">
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Buscar conversa..."
className="w-full px-3 py-2 text-sm rounded-md bg-[var(--background)] border border-[var(--border)] outline-none focus:border-[var(--accent)] transition-colors"
/>
</div>
</div>
{/* Lista de Conversas */}
<div className="flex-1 overflow-y-auto p-2 custom-scrollbar">
{loading ? (
<div className="text-center p-4 text-sm text-[var(--foreground)]/60">
Carregando...
</div>
) : conversations.length === 0 ? (
<div className="text-center p-4 text-sm text-[var(--foreground)]/60">
Nenhuma conversa ainda
</div>
) : filteredConversations.length === 0 ? (
<div className="text-center p-4 text-sm text-[var(--foreground)]/60">
Nenhum resultado para "{query}"
</div>
) : (
filteredConversations.map((conv) => (
<div
key={conv.id}
onClick={() => onSelectConversation(conv.id)}
className={`w-full flex items-center gap-2 p-3 rounded-lg mb-1 text-left transition-colors group cursor-pointer ${
currentConversationId === conv.id
? "bg-[var(--accent)]/12 text-[var(--accent)] ring-1 ring-[var(--accent)]/30"
: "hover:bg-[color-mix(in_oklab,var(--surface),black_6%)]"
}`}
>
<MessageSquare className="h-4 w-4 flex-shrink-0" />
<span className="flex-1 text-sm truncate">{conv.title}</span>
<button
onClick={(e) => handleDelete(conv.id, e)}
className="p-1 hover:bg-[var(--background)] rounded opacity-0 group-hover:opacity-100 transition-opacity"
>
<Trash2 className="h-3 w-3" />
</button>
</div>
))
)}
</div>
</div>
);
}