import type { HttpResponse } from "@yaakapp-internal/models"; import { extractSseValueAtPath, type ServerSentEvent } from "@yaakapp-internal/sse"; import { HStack, Icon, InlineCode, SplitLayout, VStack } from "@yaakapp-internal/ui"; import classNames from "classnames"; import type { CSSProperties, ReactNode } from "react"; import { Fragment, useMemo, useState } from "react"; import { useKeyValue } from "../../hooks/useKeyValue"; import { useFormatText } from "../../hooks/useFormatText"; import { useResponseBodyEventSource } from "../../hooks/useResponseBodyEventSource"; import { useResponseBodySseSummary } from "../../hooks/useResponseBodySseSummary"; import { sseSummaryResultKeyPathAutocomplete, useSseSummaryResultKeyPath, } from "../../hooks/useSseSummaryResultKeyPath"; import { isJSON } from "../../lib/contentType"; import { EmptyStateText } from "../EmptyStateText"; import { Markdown } from "../Markdown"; import { Button } from "../core/Button"; import type { DropdownItem } from "../core/Dropdown"; import { Dropdown } from "../core/Dropdown"; import type { EditorProps } from "../core/Editor/Editor"; import { Editor } from "../core/Editor/LazyEditor"; import { EventDetailHeader, EventViewer } from "../core/EventViewer"; import { EventViewerRow } from "../core/EventViewerRow"; import { IconButton } from "../core/IconButton"; import { IconTooltip } from "../core/IconTooltip"; import { Input } from "../core/Input"; import { Select } from "../core/Select"; interface Props { response: HttpResponse; } const DEFAULT_EXTRACTED_TEXT_RATIO = 0.28; export function EventStreamViewer({ response }: Props) { return ( ); } function ActualEventStreamViewer({ response }: Props) { const [showLarge, setShowLarge] = useState(false); const [showingLarge, setShowingLarge] = useState(false); const filterEventPreviewsSetting = useKeyValue({ namespace: "no_sync", key: ["sse_filter_event_previews", response.requestId], fallback: false, }); const applyToDetailsSetting = useKeyValue({ namespace: "no_sync", key: ["sse_apply_to_details", response.requestId], fallback: false, }); const renderMarkdownSetting = useKeyValue({ namespace: "no_sync", key: ["sse_render_markdown", response.requestId], fallback: false, }); const summarySettings = useSseSummaryResultKeyPath({ response }); const events = useResponseBodyEventSource(response); const summary = useResponseBodySseSummary(response, summarySettings.resultKeyPath); const showExtractedText = summarySettings.resultKeyPath != null; const showResultKeyPathWarning = showExtractedText && summary.data != null && summary.data.fragmentCount === 0 && !summary.isFetching && summary.error == null; const filterEventPreviews = showExtractedText && filterEventPreviewsSetting.value === true; const applyToDetails = showExtractedText && applyToDetailsSetting.value === true; const renderMarkdown = showExtractedText && renderMarkdownSetting.value === true; const settingsItems = useMemo( () => [ { label: "Apply to Previews", keepOpenOnSelect: true, onSelect: () => filterEventPreviewsSetting.set(filterEventPreviewsSetting.value !== true), leftSlot: ( ), }, { label: "Apply to Details", keepOpenOnSelect: true, onSelect: () => applyToDetailsSetting.set(applyToDetailsSetting.value !== true), leftSlot: ( ), }, ], [ applyToDetailsSetting, filterEventPreviewsSetting, ], ); return (
) : null } stateKey={`sse-summary-result-key-path::${response.requestId}`} tint={showResultKeyPathWarning ? "notice" : undefined} onChange={summarySettings.setResultKeyPath} />
)} (
String(index)} error={events.error ? String(events.error) : null} splitLayoutStorageKey="sse_events" defaultRatio={0.4} renderRow={({ event, index, isActive, onClick }) => ( } content={ {getEventPreview(event, summarySettings.resultKeyPath, filterEventPreviews)} } /> )} renderDetail={({ event, index, onClose }) => ( )} />
)} secondSlot={ showExtractedText ? ({ style }) => ( ) : null } /> ); } function SseSummaryFooter({ error, fragmentCount, isLoading, onRenderMarkdownChange, renderMarkdown, resultKeyPath, style, summary, }: { error: string | null; fragmentCount: number; isLoading: boolean; onRenderMarkdownChange: (renderMarkdown: boolean) => void; renderMarkdown: boolean; resultKeyPath: string; style: CSSProperties; summary: string; }) { const hasSummary = fragmentCount > 0; const actions = useMemo( () => [ { key: "sse-summary-format", label: "Extracted text format", type: "select" as const, value: renderMarkdown ? "markdown" : "text", options: [ { label: "Text", value: "text" }, { label: "Markdown", value: "markdown" }, ], onChange: (value: string) => onRenderMarkdownChange(value === "markdown"), }, ], [onRenderMarkdownChange, renderMarkdown], ); return (
{error != null ? ( {error} ) : isLoading ? ( Loading extracted text... ) : hasSummary ? ( renderMarkdown ? (
{summary}
) : (
              {summary}
            
) ) : ( No fragments for {resultKeyPath} )}
); } function getEventPreview( event: ServerSentEvent, resultKeyPath: string | null, filterEventPreview: boolean, ): string { if (filterEventPreview && resultKeyPath != null) { return (extractSseValueAtPath(event.data, resultKeyPath) ?? event.data).slice(0, 1000); } return event.data.slice(0, 1000); } function EventDetail({ applyJsonPath, event, index, resultKeyPath, showLarge, showingLarge, setShowLarge, setShowingLarge, onClose, }: { applyJsonPath: boolean; event: ServerSentEvent; index: number; resultKeyPath: string | null; showLarge: boolean; showingLarge: boolean; setShowLarge: (v: boolean) => void; setShowingLarge: (v: boolean) => void; onClose: () => void; }) { const detailText = useMemo( () => applyJsonPath && resultKeyPath != null ? (extractSseValueAtPath(event.data, resultKeyPath) ?? event.data) : event.data, [applyJsonPath, event.data, resultKeyPath], ); const language = useMemo<"text" | "json">(() => { if (!detailText) return "text"; return isJSON(detailText) ? "json" : "text"; }, [detailText]); return (
} onClose={onClose} /> {!showLarge && detailText.length > 1000 * 1000 ? ( Message previews larger than 1MB are hidden
) : ( )}
); } function FormattedEditor({ text, language }: { text: string; language: EditorProps["language"] }) { const formatted = useFormatText({ text, language, pretty: true }); if (formatted == null) return null; return ; } function EventLabels({ className, event, index, isActive, }: { event: ServerSentEvent; index: number; className?: string; isActive?: boolean; }) { return ( {event.id ?? index} {event.eventType && {event.eventType}} ); } function EventLabel({ children, isActive }: { children: ReactNode; isActive?: boolean }) { return ( {isActive && } {children} ); }