diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx index 44c80c0..c791c6d 100644 --- a/apps/web/src/main.tsx +++ b/apps/web/src/main.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useMemo, useRef, useState } from "react"; import { createRoot } from "react-dom/client"; import { BrowserRouter, Link, Navigate, NavLink, Route, Routes, useNavigate, useParams } from "react-router-dom"; -import ReactMarkdown from "react-markdown"; +import ReactMarkdown, { type Components } from "react-markdown"; import remarkGfm from "remark-gfm"; import { AlertCircle, ArrowDownRight, Bell, Boxes, Brain, CalendarClock, CheckCircle2, ChevronDown, Clipboard, Code2, Cpu, Database, Download, ExternalLink, Eye, FileText, FolderOpen, GitCompare, Globe2, Hash, History, Home, Image, KeyRound, LayoutGrid, ListTree, LoaderCircle, Maximize2, MessageSquare, Palette, Play, Plus, Search, Server, Settings, Share2, ShieldCheck, Sparkles, UserCheck, X, Zap } from "lucide-react"; import Auth1 from "./components/blocks/auth-1.js"; @@ -4763,6 +4763,22 @@ function MarkdownLink(props: MarkdownAnchorProps) { ); } +// Render model-authored answer text as markdown — bold, nested bullet lists, +// inline code, tables, links — instead of flattening it into one run-on +// paragraph with literal `**`/`-` characters. `inline` unwraps the top-level +//

so short single-line items (e.g. key points) sit directly inside their +//

  • . Some answers arrive with escaped "\n", so normalise those first. +const MARKDOWN_INLINE_COMPONENTS: Components = { a: MarkdownLink, p: ({ children }) => <>{children} }; +const MARKDOWN_BLOCK_COMPONENTS: Components = { a: MarkdownLink }; +function MarkdownText({ children, inline = false }: { children: string; inline?: boolean }) { + const normalized = children.replace(/\\r\\n|\\r|\\n/g, "\n"); + return ( + + {normalized} + + ); +} + type SdkCredentialImportFormProps = { authenticated: boolean; authBusy: boolean; @@ -5642,7 +5658,9 @@ function AiQueryPanel({ } function AiResultData({ data }: { data: Record }) { - const entries = Object.entries(data ?? {}); + // Drop empty sections (e.g. no key points) entirely rather than rendering a + // card with a lonely "No items found." placeholder. + const entries = Object.entries(data ?? {}).filter(([, value]) => !isEmptyAiValue(value)); if (!entries.length) return
    No structured data returned.
    ; return ( @@ -5660,14 +5678,15 @@ function AiResultData({ data }: { data: Record }) { function AiResultValue({ value }: { value: unknown }) { if (Array.isArray(value)) { const items = value.map((item) => stringifyAiValue(item)).filter(Boolean); - return items.length ? ( -
      + if (!items.length) return null; + return ( +
        {items.map((item, index) => ( -
      • {item}
      • +
      • + {item} +
      • ))}
      - ) : ( -

      No items found.

      ); } @@ -5676,12 +5695,10 @@ function AiResultValue({ value }: { value: unknown }) { } const text = stringifyAiValue(value); - if (!text) return

      No answer returned.

      ; + if (!text) return null; return (
      - {splitAiParagraphs(text).map((paragraph, index) => ( -

      {paragraph}

      - ))} + {text}
      ); } @@ -5693,12 +5710,12 @@ function stringifyAiValue(value: unknown): string { return JSON.stringify(value); } -function splitAiParagraphs(text: string): string[] { - return text - .replaceAll(/\\n/g, "\n") - .split(/\n{2,}/g) - .map((part) => part.replace(/\s+/g, " ").trim()) - .filter(Boolean); +function isEmptyAiValue(value: unknown): boolean { + if (value == null) return true; + if (typeof value === "string") return value.trim().length === 0; + if (Array.isArray(value)) return value.map((item) => stringifyAiValue(item)).filter(Boolean).length === 0; + if (typeof value === "object") return Object.keys(value as Record).length === 0; + return false; } function formatAiFieldLabel(value: string): string { diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css index 803bb86..12caefb 100644 --- a/apps/web/src/styles.css +++ b/apps/web/src/styles.css @@ -11411,3 +11411,89 @@ textarea:focus { .factsEntityMentions { font-family: var(--cm-mono); font-size: 10.5px; } .factsEntityDesc { font-size: 13px; line-height: 1.5; color: #4a5563; } .factsSalienceTrack { height: 5px; background: rgba(17, 20, 22, 0.06); margin-top: 3px; } + +/* Markdown rendering inside AI answer cards (grounded chat ANSWER / KEY POINTS, + run AI summary). The model returns markdown — render bold, nested bullet + lists, inline code, tables and links cleanly instead of flattening to a + run-on paragraph. Scoped to the answer surfaces only. */ +.aiTextAnswer { display: grid; gap: 9px; } +.aiTextAnswer > :first-child { margin-top: 0; } +.aiTextAnswer > :last-child { margin-bottom: 0; } +.aiTextAnswer p, +.aiTextAnswer li { margin: 0; color: #263244; font-size: 14px; line-height: 1.6; } +.aiTextAnswer strong, +.aiAnswerList strong { font-weight: 680; color: var(--cm-ink); } +.aiTextAnswer em { font-style: italic; } +.aiTextAnswer a, +.aiAnswerList a { color: var(--olive); text-decoration: underline; text-underline-offset: 2px; } +.aiTextAnswer a:hover, +.aiAnswerList a:hover { color: var(--mint); } +.aiTextAnswer ul, +.aiTextAnswer ol { display: grid; gap: 6px; margin: 2px 0; padding-left: 20px; } +.aiTextAnswer ul { list-style: disc; } +.aiTextAnswer ol { list-style: decimal; } +.aiTextAnswer li::marker { color: var(--olive); } +.aiTextAnswer li > ul, +.aiTextAnswer li > ol { margin-top: 6px; } +.aiTextAnswer li > ul { list-style: circle; } +.aiTextAnswer li p { margin: 0; } +.aiTextAnswer h1, +.aiTextAnswer h2, +.aiTextAnswer h3, +.aiTextAnswer h4 { + margin: 4px 0 0; + font-family: var(--cm-display); + font-weight: 640; + letter-spacing: -0.01em; + color: var(--cm-ink); + line-height: 1.3; +} +.aiTextAnswer h1 { font-size: 17px; } +.aiTextAnswer h2 { font-size: 15.5px; } +.aiTextAnswer h3, +.aiTextAnswer h4 { font-size: 14px; } +.aiTextAnswer :not(pre) > code, +.aiAnswerList :not(pre) > code { + font-family: var(--cm-mono); + font-size: 12px; + padding: 1.5px 5px; + border-radius: 5px; + background: rgba(17, 20, 22, 0.05); + border: 1px solid var(--cm-border); + color: #1f4d2e; +} +.aiTextAnswer pre { + margin: 2px 0; + padding: 12px 14px; + overflow: auto; + border-radius: 10px; + background: #0f172a; + border: 1px solid #1e293b; +} +.aiTextAnswer pre code { font-family: var(--cm-mono); font-size: 12.5px; line-height: 1.55; color: #e2e8f0; } +.aiTextAnswer blockquote { + margin: 2px 0; + padding: 4px 0 4px 12px; + border-left: 3px solid var(--lime-soft); + color: var(--cm-muted); +} +.aiTextAnswer table { + width: 100%; + border-collapse: collapse; + font-size: 13px; +} +.aiTextAnswer th, +.aiTextAnswer td { + padding: 7px 10px; + border: 1px solid var(--cm-border); + text-align: left; + vertical-align: top; +} +.aiTextAnswer th { font-family: var(--cm-mono); font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; color: var(--cm-muted); background: rgba(17, 20, 22, 0.03); } +.aiTextAnswer hr { border: none; border-top: 1px solid var(--cm-border); margin: 4px 0; } + +/* KEY POINTS list — markdown rendered inline inside each
    • . */ +.aiAnswerList { display: grid; gap: 7px; margin: 0; padding-left: 20px; list-style: disc; } +.aiAnswerList li { margin: 0; color: #263244; font-size: 14px; line-height: 1.55; } +.aiAnswerList li::marker { color: var(--olive); } +.aiAnswerList li :not(pre) > code { font-family: var(--cm-mono); font-size: 12px; padding: 1.5px 5px; border-radius: 5px; background: rgba(17, 20, 22, 0.05); border: 1px solid var(--cm-border); color: #1f4d2e; }