import { useMemo, useState, useEffect, useRef, type OctaneNode } from "octane"; import { Lexer, type Token, type Tokens } from "marked"; import { decodeHTML } from "entities"; import { CodeHighlighted } from "octane-kumo/code"; import { Button } from "octane-kumo/components/button"; import type { UIMessage } from "ai"; import type { ToolActivity } from "../../shared/tool-activity"; /** Application links have a deliberately smaller surface than external links. */ export function safeChatUrl( value: string, application = true, ): string | undefined { const url = decodeHTML(value); if (/[\u0000-\u0020\\]/.test(url)) return; if ( application && /^\/(tasks|conversations)\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test( url, ) ) return url; try { const parsed = new URL(url); if ( !["http:", "https:"].includes(parsed.protocol) || parsed.username || parsed.password ) return; return parsed.href; } catch { return; } } function SafeLink({ href, children }: { href: string; children: OctaneNode }) { const safe = safeChatUrl(href); return safe ? ( {children} ) : ( {children} ); } function Code({ code, language, streaming, }: { code: string; language: string; streaming: boolean; }) { const [notice, setNotice] = useState(""); const mounted = useRef(false); useEffect(() => { mounted.current = true; return () => { mounted.current = false; }; }, []); return (
{language || "Plain text"} {notice}
{streaming || code.length > 40_000 ? (
          {code}
        
) : ( )}
); } function renderTokens( tokens: Token[], streaming: boolean, depth = 0, ): OctaneNode { if (depth > 32) return tokens.map((token) => token.raw).join(""); return tokens.map((token, index) => { const child = (children: Token[]) => renderTokens(children, streaming, depth + 1); switch (token.type) { case "space": case "def": return null; case "heading": return

{child((token as Tokens.Heading).tokens)}

; case "paragraph": return

{child((token as Tokens.Paragraph).tokens)}

; case "text": { const text = token as Tokens.Text; return text.tokens ? child(text.tokens) : decodeHTML(text.text); } case "escape": return decodeHTML((token as Tokens.Escape).text); case "html": return {token.raw}; case "strong": return ( {child((token as Tokens.Strong).tokens)} ); case "em": return {child((token as Tokens.Em).tokens)}; case "del": return {child((token as Tokens.Del).tokens)}; case "codespan": return ( {decodeHTML((token as Tokens.Codespan).text)} ); case "code": { const code = token as Tokens.Code; return ( ); } case "link": { const link = token as Tokens.Link; return ( {child(link.tokens)} ); } case "image": return ( [Image: {decodeHTML((token as Tokens.Image).text)}] ); case "br": return
; case "hr": return
; case "blockquote": return (
{child((token as Tokens.Blockquote).tokens)}
); case "list": { const list = token as Tokens.List; const items = list.items.map((item, i) => (
  • {item.task ? ( {item.checked ? "☑ " : "☐ "} ) : null} {child(item.tokens)}
  • )); return list.ordered ? (
      {items}
    ) : ( ); } case "table": { const table = token as Tokens.Table; return (
    {table.header.map((cell, i) => ( ))} {table.rows.map((row, i) => ( {row.map((cell, j) => ( ))} ))}
    {child(cell.tokens)}
    {child(cell.tokens)}
    ); } default: return {token.raw}; } }); } function Markdown({ text, streaming }: { text: string; streaming: boolean }) { const tokens = useMemo(() => Lexer.lex(text, { gfm: true }), [text]); return
    {renderTokens(tokens, streaming)}
    ; } type ToolPart = UIMessage["parts"][number] & { toolCallId: string; state: string; toolName?: string; input?: unknown; output?: unknown; errorText?: string; }; function detail(value: unknown): string { if (value === undefined) return "Not available yet."; try { const text = JSON.stringify(value, null, 2); return text.length > 20_000 ? `${text.slice(0, 20_000)}\n… Detail truncated.` : text; } catch { return "Detail unavailable."; } } function ToolRow({ part, activity, }: { part: ToolPart; activity?: ToolActivity; }) { const [open, setOpen] = useState(false); const fallback = part.state === "output-error" || part.state === "output-denied" ? "failed" : part.state === "output-available" ? "result available" : "running"; const status = activity?.status ?? fallback; const result = part.output && typeof part.output === "object" ? (part.output as { url?: unknown; status?: unknown }) : undefined; const taskUrl = typeof result?.url === "string" && result.url.startsWith("/tasks/") ? safeChatUrl(result.url) : undefined; return (
    setOpen(event.currentTarget.open)} > {activity?.inputSummary || part.toolName || part.type.replace(/^tool-/, "")} {status} {activity?.progress && status === "running" ? ( {activity.progress.text} ) : null} {activity?.outputSummary ?

    {activity.outputSummary}

    : null} {taskUrl ? (

    View scheduled task {result?.status === "unavailable" ? " — schedule unavailable" : result?.status === "inactive" ? " — inactive" : ""}

    ) : null} {open ? (
    {activity && activity.attempts > 1 ? (

    Attempt {activity.attempts} {activity.interruptedAt ? " · resumed after interruption" : ""}

    ) : null}

    Input

    {detail(part.input)}

    Result

    {part.errorText ?? detail(part.output)}
    ) : null}
    ); } function sources(message: UIMessage) { const result = new Map< string, { url: string; title: string; kind: string } >(); const add = (url: unknown, title: unknown, kind: string) => { if (typeof url !== "string") return; const safe = safeChatUrl(url, false); if (safe && !result.has(safe)) result.set(safe, { url: safe, title: typeof title === "string" && title ? title : new URL(safe).hostname, kind, }); }; for (const part of message.parts) { if (part.type === "source-url") add(part.url, part.title, "Source"); if ( "output" in part && part.output && typeof part.output === "object" && "sources" in part.output && Array.isArray(part.output.sources) ) { for (const source of part.output.sources) if (source && typeof source === "object") add( source.finalUrl, source.title, source.sourceKind === "search" ? "Search result" : source.sourceKind === "browser" ? "Browser page" : "Read page", ); } } return [...result.values()]; } export function ChatMessage({ message, activities, streaming, }: { message: UIMessage; activities: ReadonlyMap; streaming: boolean; }) { const citations = useMemo(() => sources(message), [message]); return (

    {message.role === "user" ? "You" : "Flarebot"}

    {message.parts.map((part, index) => part.type === "text" ? ( message.role === "user" ? (

    {part.text}

    ) : ( ) ) : "toolCallId" in part ? ( ) : null, )} {citations.length ? (
    Sources ({citations.length})
    ) : null}
    ); }