Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 34 additions & 17 deletions apps/web/src/main.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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
// <p> so short single-line items (e.g. key points) sit directly inside their
// <li>. 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 (
<ReactMarkdown remarkPlugins={[remarkGfm]} components={inline ? MARKDOWN_INLINE_COMPONENTS : MARKDOWN_BLOCK_COMPONENTS}>
{normalized}
</ReactMarkdown>
);
}

type SdkCredentialImportFormProps = {
authenticated: boolean;
authBusy: boolean;
Expand Down Expand Up @@ -5642,7 +5658,9 @@ function AiQueryPanel({
}

function AiResultData({ data }: { data: Record<string, unknown> }) {
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 <div className="subEmpty aiResultEmpty">No structured data returned.</div>;

return (
Expand All @@ -5660,14 +5678,15 @@ function AiResultData({ data }: { data: Record<string, unknown> }) {
function AiResultValue({ value }: { value: unknown }) {
if (Array.isArray(value)) {
const items = value.map((item) => stringifyAiValue(item)).filter(Boolean);
return items.length ? (
<ul>
if (!items.length) return null;
return (
<ul className="aiAnswerList">
{items.map((item, index) => (
<li key={`${item}-${index}`}>{item}</li>
<li key={`${item}-${index}`}>
<MarkdownText inline>{item}</MarkdownText>
</li>
))}
</ul>
) : (
<p className="mutedText">No items found.</p>
);
}

Expand All @@ -5676,12 +5695,10 @@ function AiResultValue({ value }: { value: unknown }) {
}

const text = stringifyAiValue(value);
if (!text) return <p className="mutedText">No answer returned.</p>;
if (!text) return null;
return (
<div className="aiTextAnswer">
{splitAiParagraphs(text).map((paragraph, index) => (
<p key={`${paragraph}-${index}`}>{paragraph}</p>
))}
<MarkdownText>{text}</MarkdownText>
</div>
);
}
Expand All @@ -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<string, unknown>).length === 0;
return false;
}

function formatAiFieldLabel(value: string): string {
Expand Down
86 changes: 86 additions & 0 deletions apps/web/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 <li>. */
.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; }
Loading