diff --git a/web/index.html b/web/index.html index 1d31b39..262d2fa 100644 --- a/web/index.html +++ b/web/index.html @@ -3,7 +3,7 @@ - Sybil Chat + Sybil
diff --git a/web/src/App.tsx b/web/src/App.tsx index e35fd3a..c996d81 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -21,6 +21,7 @@ import { type ChatDetail, type ChatSummary, type CompletionRequestMessage, + type Message, type SearchDetail, type SearchSummary, } from "@/lib/api"; @@ -113,6 +114,7 @@ export default function App() { const [isLoadingCollections, setIsLoadingCollections] = useState(false); const [isLoadingSelection, setIsLoadingSelection] = useState(false); const [isSending, setIsSending] = useState(false); + const [pendingChatState, setPendingChatState] = useState<{ chatId: string | null; messages: Message[] } | null>(null); const [composer, setComposer] = useState(""); const [provider, setProvider] = useState("openai"); const [model, setModel] = useState(PROVIDER_DEFAULT_MODELS.openai); @@ -132,6 +134,7 @@ export default function App() { setSelectedChat(null); setSelectedSearch(null); setDraftKind(null); + setPendingChatState(null); setComposer(""); setError(null); }; @@ -251,6 +254,18 @@ export default function App() { }, []); const messages = selectedChat?.messages ?? []; + const isSearchMode = draftKind ? draftKind === "search" : selectedItem?.kind === "search"; + const isSearchRunning = isSending && isSearchMode; + const displayMessages = useMemo(() => { + if (!pendingChatState) return messages; + if (pendingChatState.chatId) { + if (selectedItem?.kind === "chat" && selectedItem.id === pendingChatState.chatId) { + return pendingChatState.messages; + } + return messages; + } + return isSearchMode ? messages : pendingChatState.messages; + }, [isSearchMode, messages, pendingChatState, selectedItem]); const selectedChatSummary = useMemo(() => { if (!selectedItem || selectedItem.kind !== "chat") return null; @@ -276,9 +291,6 @@ export default function App() { return "New search"; }, [draftKind, selectedChat, selectedChatSummary, selectedItem, selectedSearch, selectedSearchSummary]); - const isSearchMode = draftKind ? draftKind === "search" : selectedItem?.kind === "search"; - const isSearchRunning = isSending && isSearchMode; - const handleCreateChat = () => { setError(null); setContextMenu(null); @@ -347,6 +359,27 @@ export default function App() { }, [contextMenu]); const handleSendChat = async (content: string) => { + const optimisticUserMessage: Message = { + id: `temp-user-${Date.now()}`, + createdAt: new Date().toISOString(), + role: "user", + content, + name: null, + }; + + const optimisticAssistantMessage: Message = { + id: `temp-assistant-${Date.now()}`, + createdAt: new Date().toISOString(), + role: "assistant", + content: "", + name: null, + }; + + setPendingChatState({ + chatId: selectedItem?.kind === "chat" ? selectedItem.id : null, + messages: (selectedChat?.messages ?? []).concat(optimisticUserMessage, optimisticAssistantMessage), + }); + let chatId = draftKind === "chat" ? null : selectedItem?.kind === "chat" ? selectedItem.id : null; if (!chatId) { @@ -354,6 +387,7 @@ export default function App() { chatId = chat.id; setDraftKind(null); setSelectedItem({ kind: "chat", id: chatId }); + setPendingChatState((current) => (current ? { ...current, chatId } : current)); setSelectedChat({ id: chat.id, title: chat.title, @@ -373,30 +407,6 @@ export default function App() { baseChat = await getChat(chatId); } - const optimisticUserMessage = { - id: `temp-user-${Date.now()}`, - createdAt: new Date().toISOString(), - role: "user" as const, - content, - name: null, - }; - - const optimisticAssistantMessage = { - id: `temp-assistant-${Date.now()}`, - createdAt: new Date().toISOString(), - role: "assistant" as const, - content: "", - name: null, - }; - - setSelectedChat((current) => { - if (!current || current.id !== chatId) return current; - return { - ...current, - messages: [...current.messages, optimisticUserMessage, optimisticAssistantMessage], - }; - }); - const requestMessages: CompletionRequestMessage[] = [ ...baseChat.messages.map((message) => ({ role: message.role, @@ -417,6 +427,7 @@ export default function App() { }); await Promise.all([refreshCollections({ kind: "chat", id: chatId }), refreshChat(chatId)]); + setPendingChatState(null); }; const handleSendSearch = async (query: string) => { @@ -569,6 +580,10 @@ export default function App() { setError(message); } + if (!isSearchMode) { + setPendingChatState(null); + } + if (selectedItem?.kind === "chat") { await refreshChat(selectedItem.id); } @@ -700,7 +715,7 @@ export default function App() {
{!isSearchMode ? ( - + ) : ( )} diff --git a/web/src/components/chat/chat-messages-panel.tsx b/web/src/components/chat/chat-messages-panel.tsx index 85e591a..ee6cb37 100644 --- a/web/src/components/chat/chat-messages-panel.tsx +++ b/web/src/components/chat/chat-messages-panel.tsx @@ -1,5 +1,6 @@ import { cn } from "@/lib/utils"; import type { Message } from "@/lib/api"; +import { MarkdownContent } from "@/components/markdown/markdown-content"; type Props = { messages: Message[]; @@ -11,12 +12,6 @@ export function ChatMessagesPanel({ messages, isLoading, isSending }: Props) { return ( <> {isLoading && messages.length === 0 ?

Loading messages...

: null} - {!isLoading && messages.length === 0 ? ( -
-

How can I help today?

-

Ask a question to begin this conversation.

-
- ) : null}
{messages.map((message) => { const isUser = message.role === "user"; @@ -25,11 +20,22 @@ export function ChatMessagesPanel({ messages, isLoading, isSending }: Props) {
- {isPendingAssistant ? "Thinking..." : message.content} + {isPendingAssistant ? ( + + + + + + ) : ( + + )}
); diff --git a/web/src/components/search/search-results-panel.tsx b/web/src/components/search/search-results-panel.tsx index adc8696..59207c2 100644 --- a/web/src/components/search/search-results-panel.tsx +++ b/web/src/components/search/search-results-panel.tsx @@ -1,5 +1,4 @@ import { useEffect, useRef, useState } from "preact/hooks"; -import { Search } from "lucide-preact"; import type { SearchDetail } from "@/lib/api"; import { MarkdownContent } from "@/components/markdown/markdown-content"; import { cn } from "@/lib/utils"; @@ -27,11 +26,10 @@ type Props = { search: SearchDetail | null; isLoading: boolean; isRunning: boolean; - showPrompt?: boolean; className?: string; }; -export function SearchResultsPanel({ search, isLoading, isRunning, showPrompt = true, className }: Props) { +export function SearchResultsPanel({ search, isLoading, isRunning, className }: Props) { const ANSWER_COLLAPSED_HEIGHT_CLASS = "h-[3rem]"; const [isAnswerExpanded, setIsAnswerExpanded] = useState(false); const [canExpandAnswer, setCanExpandAnswer] = useState(false); @@ -159,14 +157,6 @@ export function SearchResultsPanel({ search, isLoading, isRunning, showPrompt =

{isRunning ? "Searching Exa..." : "Loading search..."}

) : null} - {showPrompt && !isLoading && !search?.query ? ( -
- -

Search the web

-

Use the composer below to run a new Exa search.

-
- ) : null} - {!isLoading && !isRunning && !!search?.query && search.results.length === 0 ? (

No results found.

) : null} diff --git a/web/src/index.css b/web/src/index.css index 662d1c7..d45ce37 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -3,7 +3,7 @@ @tailwind utilities; :root { - --background: 282 33% 10%; + --background: 282 33% 8%; --foreground: 300 35% 95%; --muted: 287 24% 16%; --muted-foreground: 297 16% 72%; @@ -31,7 +31,7 @@ body, body { @apply bg-background text-foreground antialiased; - background-image: radial-gradient(circle at top, hsl(274 42% 20%) 0%, hsl(271 34% 14%) 42%, hsl(266 32% 9%) 100%); + background-image: radial-gradient(circle at top, hsl(274 42% 18%) 0%, hsl(271 34% 12%) 42%, hsl(266 32% 7%) 100%); font-family: "Soehne", "Avenir Next", "Segoe UI", sans-serif; } @@ -43,10 +43,33 @@ body { margin-top: 0.85rem; } +.md-content h1, +.md-content h2, +.md-content h3 { + line-height: 1.25; + margin-top: 1.2rem; + margin-bottom: 0.45rem; + font-weight: 700; +} + +.md-content h1 { + font-size: 1.45rem; +} + +.md-content h2 { + font-size: 1.25rem; +} + +.md-content h3 { + font-size: 1.1rem; +} + .md-content ul, .md-content ol { margin-top: 0.65rem; - margin-left: 1.25rem; + margin-left: 0; + padding-left: 0; + list-style-position: inside; } .md-content li + li { diff --git a/web/src/pages/search-route-page.tsx b/web/src/pages/search-route-page.tsx index f732ff5..2b274ab 100644 --- a/web/src/pages/search-route-page.tsx +++ b/web/src/pages/search-route-page.tsx @@ -228,7 +228,7 @@ export default function SearchRoutePage() { {error ?

{error}

: null} - +
);