harryagasi commited on
Commit
f18b03d
·
1 Parent(s): 9932e86

fix: stabilize analysis chat rendering and traceability

Browse files

- Preserve leading spaces in Python SSE stream chunks
- Improve AI markdown rendering, spacing, wrapping, and malformed bold handling
- Persist cleaned assistant answers to Golang message history
- Rehydrate traceability after refreshing saved conversations
- Keep chat input fixed while only the message list scrolls
- Silence empty report-version lookup errors before report generation
- Improve dark-mode contrast for emerald surfaces

src/app/components/analysis/AnalysisShell.tsx CHANGED
@@ -41,6 +41,13 @@ function toUiMessage(message: AnalysisMessage): UiMessage {
41
 
42
  const wait = (ms: number) => new Promise((resolve) => window.setTimeout(resolve, ms));
43
 
 
 
 
 
 
 
 
44
  export function AnalysisShell() {
45
  const navigate = useNavigate();
46
  const session = useMemo(() => getCurrentSession(), []);
@@ -144,6 +151,23 @@ export function AnalysisShell() {
144
  }
145
  }
146
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
 
148
  const runAgentStream = async (kind: "chat" | "help", prompt?: string) => {
149
  if (!session?.user_id || !activeAnalysis || streamState !== "idle") return;
@@ -195,13 +219,18 @@ export function AnalysisShell() {
195
 
196
  if (streamError) return;
197
 
 
198
  let traceTargetId = aiId;
199
  setMessages((prev) =>
200
- prev.map((msg) => (msg.id === aiId ? { ...msg, status: "complete", messageId: doneMessageId, traceabilityLoading: Boolean(doneMessageId) } : msg))
 
 
 
 
201
  );
202
 
203
- if (finalContent.trim()) {
204
- const saved = await createAnalysisMessage(activeAnalysis.id, { role: "ai", content: finalContent.trim(), message_id: doneMessageId });
205
  traceTargetId = saved.id;
206
  setMessages((prev) =>
207
  prev.map((msg) =>
@@ -291,10 +320,10 @@ export function AnalysisShell() {
291
  activeMenu === "analysis-agent" ? "Analysis Agent" : activeMenu === "knowledge" ? "Knowledge" : "Home";
292
 
293
  const renderAnalysisAgent = () => (
294
- <div className={cx("grid min-h-screen grid-cols-1", reportCollapsed ? "lg:grid-cols-[minmax(0,1fr)_3.5rem]" : "lg:grid-cols-[minmax(0,1fr)_23rem]")}>
295
- <main className="flex min-h-[70vh] min-w-0 flex-col bg-slate-50">
296
  <AnalysisHeader analysis={activeAnalysis} staleSources={staleSources} onUpdateDataBind={handleUpdateDataBind} />
297
- <div className="min-h-0 flex-1 overflow-y-auto">
298
  {loadingMessages ? (
299
  <div className="flex h-full items-center justify-center text-sm text-slate-500">Loading messages</div>
300
  ) : (
@@ -408,7 +437,7 @@ export function AnalysisShell() {
408
  </div>
409
  )}
410
 
411
- <div className={cx("lg:grid lg:min-h-screen", desktopNavCollapsed ? "lg:grid-cols-[4.5rem_minmax(0,1fr)]" : "lg:grid-cols-[16rem_minmax(0,1fr)]")}>
412
  <div className="hidden lg:block">
413
  <AppNavigation
414
  active={activeMenu}
@@ -426,7 +455,7 @@ export function AnalysisShell() {
426
  />
427
  </div>
428
 
429
- <div className="min-w-0">
430
  {activeMenu === "home" && <HomeDashboard hasAnalyses={analyses.length > 0} onNavigate={changeMenu} />}
431
  {activeMenu === "knowledge" && <KnowledgeManagement open onClose={() => changeMenu("home")} variant="page" />}
432
  {activeMenu === "analysis-agent" && renderAnalysisAgent()}
 
41
 
42
  const wait = (ms: number) => new Promise((resolve) => window.setTimeout(resolve, ms));
43
 
44
+ function normalizeAssistantContent(content: string) {
45
+ return content
46
+ .replace(/\*\*\s+([^*\n]*?\S)\s*\*\*/g, "**$1**")
47
+ .replace(/\*\*([^*\n]*?\S)\s+\*\*/g, "**$1**")
48
+ .trim();
49
+ }
50
+
51
  export function AnalysisShell() {
52
  const navigate = useNavigate();
53
  const session = useMemo(() => getCurrentSession(), []);
 
151
  }
152
  }
153
  };
154
+ useEffect(() => {
155
+ if (!activeAnalysis?.id || loadingMessages) return;
156
+
157
+ messages
158
+ .filter(
159
+ (message) =>
160
+ message.role === "ai" &&
161
+ message.status === "complete" &&
162
+ Boolean(message.messageId) &&
163
+ !message.traceability &&
164
+ !message.traceabilityLoading &&
165
+ !message.traceabilityError
166
+ )
167
+ .forEach((message) => {
168
+ fetchTraceability(activeAnalysis.id, message.messageId, message.id);
169
+ });
170
+ }, [activeAnalysis?.id, loadingMessages, messages]);
171
 
172
  const runAgentStream = async (kind: "chat" | "help", prompt?: string) => {
173
  if (!session?.user_id || !activeAnalysis || streamState !== "idle") return;
 
219
 
220
  if (streamError) return;
221
 
222
+ const completedContent = normalizeAssistantContent(finalContent);
223
  let traceTargetId = aiId;
224
  setMessages((prev) =>
225
+ prev.map((msg) =>
226
+ msg.id === aiId
227
+ ? { ...msg, content: completedContent || msg.content, status: "complete", messageId: doneMessageId, traceabilityLoading: Boolean(doneMessageId) }
228
+ : msg
229
+ )
230
  );
231
 
232
+ if (completedContent) {
233
+ const saved = await createAnalysisMessage(activeAnalysis.id, { role: "ai", content: completedContent, message_id: doneMessageId });
234
  traceTargetId = saved.id;
235
  setMessages((prev) =>
236
  prev.map((msg) =>
 
320
  activeMenu === "analysis-agent" ? "Analysis Agent" : activeMenu === "knowledge" ? "Knowledge" : "Home";
321
 
322
  const renderAnalysisAgent = () => (
323
+ <div className={cx("grid h-full min-h-0 grid-cols-1", reportCollapsed ? "lg:grid-cols-[minmax(0,1fr)_3.5rem]" : "lg:grid-cols-[minmax(0,1fr)_23rem]")}>
324
+ <main className="flex h-full min-h-0 min-w-0 flex-col bg-slate-50">
325
  <AnalysisHeader analysis={activeAnalysis} staleSources={staleSources} onUpdateDataBind={handleUpdateDataBind} />
326
+ <div className="min-h-0 flex-1 overflow-y-auto overflow-x-hidden overscroll-contain">
327
  {loadingMessages ? (
328
  <div className="flex h-full items-center justify-center text-sm text-slate-500">Loading messages</div>
329
  ) : (
 
437
  </div>
438
  )}
439
 
440
+ <div className={cx("lg:grid", activeMenu === "analysis-agent" ? "h-[calc(100vh-3.5rem)] min-h-0 overflow-hidden lg:h-screen" : "min-h-screen", desktopNavCollapsed ? "lg:grid-cols-[4.5rem_minmax(0,1fr)]" : "lg:grid-cols-[16rem_minmax(0,1fr)]")}>
441
  <div className="hidden lg:block">
442
  <AppNavigation
443
  active={activeMenu}
 
455
  />
456
  </div>
457
 
458
+ <div className={cx("min-w-0", activeMenu === "analysis-agent" && "h-full min-h-0 overflow-hidden")}>
459
  {activeMenu === "home" && <HomeDashboard hasAnalyses={analyses.length > 0} onNavigate={changeMenu} />}
460
  {activeMenu === "knowledge" && <KnowledgeManagement open onClose={() => changeMenu("home")} variant="page" />}
461
  {activeMenu === "analysis-agent" && renderAnalysisAgent()}
src/app/components/analysis/ChatInput.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import { useState } from "react";
2
  import { Loader2, Send } from "lucide-react";
3
  import { HelpSkillButton } from "./HelpSkillButton";
4
 
@@ -21,8 +21,8 @@ export function ChatInput({ disabled, streaming, onSend, onHelp }: ChatInputProp
21
  };
22
 
23
  return (
24
- <form onSubmit={submit} className="border-t border-slate-200 bg-white p-4">
25
- <div className="mx-auto flex max-w-4xl flex-col gap-2 sm:flex-row sm:items-end">
26
  <textarea
27
  aria-label="Chat message"
28
  value={message}
@@ -36,7 +36,7 @@ export function ChatInput({ disabled, streaming, onSend, onHelp }: ChatInputProp
36
  disabled={disabled || streaming}
37
  rows={2}
38
  placeholder="Ask the agent about this analysis"
39
- className="min-h-10 flex-1 resize-none rounded-md border border-slate-200 bg-white px-3 py-2 text-sm leading-6 outline-none transition placeholder:text-slate-400 focus:border-emerald-500 focus:ring-2 focus:ring-emerald-100 disabled:bg-slate-50"
40
  />
41
  <div className="flex gap-2">
42
  <HelpSkillButton disabled={disabled || streaming} onClick={onHelp} />
 
1
+ import { useState } from "react";
2
  import { Loader2, Send } from "lucide-react";
3
  import { HelpSkillButton } from "./HelpSkillButton";
4
 
 
21
  };
22
 
23
  return (
24
+ <form onSubmit={submit} className="flex-shrink-0 border-t border-slate-200 bg-white p-4">
25
+ <div className="mx-auto flex max-w-5xl flex-col gap-2 sm:flex-row sm:items-end">
26
  <textarea
27
  aria-label="Chat message"
28
  value={message}
 
36
  disabled={disabled || streaming}
37
  rows={2}
38
  placeholder="Ask the agent about this analysis"
39
+ className="min-h-10 min-w-0 flex-1 resize-none rounded-md border border-slate-200 bg-white px-3 py-2 text-sm leading-6 text-slate-900 outline-none transition placeholder:text-slate-400 focus:border-emerald-500 focus:ring-2 focus:ring-emerald-100 disabled:bg-slate-50"
40
  />
41
  <div className="flex gap-2">
42
  <HelpSkillButton disabled={disabled || streaming} onClick={onHelp} />
src/app/components/analysis/MarkdownContent.tsx CHANGED
@@ -1,13 +1,88 @@
1
- import ReactMarkdown from "react-markdown";
 
2
  import remarkGfm from "remark-gfm";
3
  import remarkMath from "remark-math";
4
  import rehypeKatex from "rehype-katex";
5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  export function MarkdownContent({ content }: { content: string }) {
 
 
7
  return (
8
- <div className="prose prose-sm max-w-none prose-slate prose-headings:mb-2 prose-headings:mt-4 prose-p:my-2 prose-ul:my-2 prose-ol:my-2 prose-li:my-0 prose-code:rounded prose-code:bg-slate-100 prose-code:px-1 prose-code:py-0.5 prose-pre:bg-slate-950 prose-pre:text-slate-50">
9
- <ReactMarkdown remarkPlugins={[remarkGfm, remarkMath]} rehypePlugins={[rehypeKatex]}>
10
- {content}
11
  </ReactMarkdown>
12
  </div>
13
  );
 
1
+ import ReactMarkdown from "react-markdown";
2
+ import type { Components } from "react-markdown";
3
  import remarkGfm from "remark-gfm";
4
  import remarkMath from "remark-math";
5
  import rehypeKatex from "rehype-katex";
6
 
7
+ function repairMarkdownMarkers(line: string) {
8
+ return line
9
+ .replace(/\*\*\s+([^*\n]*?\S)\s*\*\*/g, "**$1**")
10
+ .replace(/\*\*([^*\n]*?\S)\s+\*\*/g, "**$1**")
11
+ .replace(/([A-Za-z0-9])(?=\*\*)/g, "$1 ")
12
+ .replace(/(\*\*[^*]+\*\*)(?=[A-Za-z0-9])/g, "$1 ");
13
+ }
14
+
15
+ function normalizeReadableMarkdown(content: string) {
16
+ const lines = content.replace(/\r\n/g, "\n").split("\n");
17
+ let inCodeBlock = false;
18
+
19
+ const normalizedLines = lines.map((line) => {
20
+ if (line.trimStart().startsWith("```")) {
21
+ inCodeBlock = !inCodeBlock;
22
+ return line;
23
+ }
24
+
25
+ if (inCodeBlock) return line;
26
+
27
+ return repairMarkdownMarkers(line)
28
+ .replace(/([.!?])\s*-\s*(?=[A-Z0-9*])/g, "$1\n\n- ")
29
+ .replace(/(^|\n)-(?=\*\*|[A-Z0-9])/g, "$1- ")
30
+ .replace(/([,;:])(?=\S)/g, "$1 ")
31
+ .replace(/([.!?])(?=[A-Z0-9])/g, "$1 ")
32
+ .replace(/[ \t]{2,}/g, " ");
33
+ });
34
+
35
+ return normalizedLines
36
+ .map((line, index) => {
37
+ const trimmed = line.trim();
38
+ if (!trimmed || /^[-*+]\s/.test(trimmed) || /^\d+\.\s/.test(trimmed)) return line;
39
+
40
+ const previous = normalizedLines.slice(0, index).reverse().find((item) => item.trim());
41
+ const next = normalizedLines.slice(index + 1).find((item) => item.trim());
42
+ const isStandaloneBold = /^\*\*.+\*\*(?:\s+.+)?$/.test(trimmed);
43
+ const belongsToBoldList = previous?.trim().endsWith(":") || previous?.trim().startsWith("**") || next?.trim().startsWith("**");
44
+
45
+ return isStandaloneBold && belongsToBoldList ? `- ${trimmed}` : line;
46
+ })
47
+ .join("\n")
48
+ .replace(/\n{3,}/g, "\n\n")
49
+ .trim();
50
+ }
51
+
52
+ const markdownComponents: Components = {
53
+ p: ({ children }) => <p className="my-3 leading-7 text-slate-800">{children}</p>,
54
+ ul: ({ children }) => <ul className="my-3 list-disc space-y-2 pl-5">{children}</ul>,
55
+ ol: ({ children }) => <ol className="my-3 list-decimal space-y-2 pl-5">{children}</ol>,
56
+ li: ({ children }) => <li className="pl-1 leading-7 text-slate-800">{children}</li>,
57
+ strong: ({ children }) => <strong className="font-semibold text-slate-950">{children}</strong>,
58
+ h1: ({ children }) => <h1 className="mb-3 mt-5 text-xl font-semibold leading-7 text-slate-950">{children}</h1>,
59
+ h2: ({ children }) => <h2 className="mb-3 mt-5 text-lg font-semibold leading-7 text-slate-950">{children}</h2>,
60
+ h3: ({ children }) => <h3 className="mb-2 mt-4 text-base font-semibold leading-6 text-slate-950">{children}</h3>,
61
+ a: ({ children, href }) => (
62
+ <a href={href} className="font-medium text-emerald-700 underline decoration-emerald-200 underline-offset-4" target="_blank" rel="noreferrer">
63
+ {children}
64
+ </a>
65
+ ),
66
+ code: ({ children, className }) => {
67
+ const isBlock = className?.includes("language-");
68
+ if (isBlock) {
69
+ return <code className={className}>{children}</code>;
70
+ }
71
+ return <code className="rounded bg-slate-100 px-1.5 py-0.5 text-[0.85em] text-slate-900">{children}</code>;
72
+ },
73
+ pre: ({ children }) => <pre className="my-3 max-w-full overflow-x-auto rounded-lg bg-slate-950 p-3 text-xs leading-6 text-slate-50">{children}</pre>,
74
+ table: ({ children }) => <table className="my-4 w-full table-auto border-collapse text-sm">{children}</table>,
75
+ th: ({ children }) => <th className="border border-slate-200 bg-slate-50 px-3 py-2 text-left font-semibold text-slate-900">{children}</th>,
76
+ td: ({ children }) => <td className="border border-slate-200 px-3 py-2 align-top text-slate-700">{children}</td>,
77
+ };
78
+
79
  export function MarkdownContent({ content }: { content: string }) {
80
+ const readableContent = normalizeReadableMarkdown(content);
81
+
82
  return (
83
+ <div className="agent-markdown max-w-none break-words text-sm leading-7 text-slate-800 [overflow-wrap:anywhere]">
84
+ <ReactMarkdown remarkPlugins={[remarkGfm, remarkMath]} rehypePlugins={[rehypeKatex]} components={markdownComponents}>
85
+ {readableContent}
86
  </ReactMarkdown>
87
  </div>
88
  );
src/app/components/analysis/MessageList.tsx CHANGED
@@ -15,22 +15,22 @@ export function MessageList({ messages }: { messages: UiMessage[] }) {
15
  }
16
 
17
  return (
18
- <div className="mx-auto flex w-full max-w-4xl flex-col gap-4 px-4 py-6">
19
  {messages.map((message) => {
20
  const isUser = message.role === "user";
21
  return (
22
- <article key={message.id} className={cx("flex gap-3", isUser && "justify-end")}>
23
  {!isUser && (
24
  <div className="mt-1 flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-md bg-slate-900 text-white">
25
  <Bot className="h-4 w-4" />
26
  </div>
27
  )}
28
- <div className={cx("max-w-[84%] rounded-lg border px-4 py-3", isUser ? "border-emerald-100 bg-emerald-50" : "border-slate-200 bg-white")}>
29
  <div className="mb-1 flex items-center justify-between gap-3 text-[11px] text-slate-400">
30
  <span className="font-medium uppercase tracking-wide">{isUser ? "You" : "AI Agent"}</span>
31
  <span>{formatDateTime(message.created_at)}</span>
32
  </div>
33
- {isUser ? <p className="whitespace-pre-wrap text-sm leading-6 text-slate-800">{message.content}</p> : <MarkdownContent content={message.content || "..."} />}
34
  {message.status === "streaming" && message.statusText && <p className="mt-2 text-xs text-slate-500">{message.statusText}</p>}
35
  {message.status === "error" && <p className="mt-2 text-xs text-red-600">{message.statusText ?? "Stream failed"}</p>}
36
  {!isUser && (
 
15
  }
16
 
17
  return (
18
+ <div className="mx-auto flex w-full max-w-5xl flex-col gap-5 px-4 py-6">
19
  {messages.map((message) => {
20
  const isUser = message.role === "user";
21
  return (
22
+ <article key={message.id} className={cx("flex min-w-0 gap-3", isUser && "justify-end")}>
23
  {!isUser && (
24
  <div className="mt-1 flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-md bg-slate-900 text-white">
25
  <Bot className="h-4 w-4" />
26
  </div>
27
  )}
28
+ <div className={cx("min-w-0 rounded-lg border px-4 py-3 shadow-sm", isUser ? "max-w-[min(36rem,84%)] border-emerald-100 bg-emerald-50" : "w-full max-w-3xl border-slate-200 bg-white")}>
29
  <div className="mb-1 flex items-center justify-between gap-3 text-[11px] text-slate-400">
30
  <span className="font-medium uppercase tracking-wide">{isUser ? "You" : "AI Agent"}</span>
31
  <span>{formatDateTime(message.created_at)}</span>
32
  </div>
33
+ {isUser ? <p className="whitespace-pre-wrap break-words text-sm leading-6 text-slate-800 [overflow-wrap:anywhere]">{message.content}</p> : <MarkdownContent content={message.content || "..."} />}
34
  {message.status === "streaming" && message.statusText && <p className="mt-2 text-xs text-slate-500">{message.statusText}</p>}
35
  {message.status === "error" && <p className="mt-2 text-xs text-red-600">{message.statusText ?? "Stream failed"}</p>}
36
  {!isUser && (
src/app/components/analysis/ReportSidebar.tsx CHANGED
@@ -30,7 +30,7 @@ export function ReportSidebar({ analysis, userId, onCollapse }: ReportSidebarPro
30
  const [error, setError] = useState<string | null>(null);
31
  const [precondition, setPrecondition] = useState<string | null>(null);
32
 
33
- const loadVersions = async (analysisId: string) => {
34
  setLoadingVersions(true);
35
  setError(null);
36
  try {
@@ -39,9 +39,12 @@ export function ReportSidebar({ analysis, userId, onCollapse }: ReportSidebarPro
39
  const latest = [...list].sort((a, b) => b.version - a.version)[0];
40
  setSelectedVersion(latest?.version);
41
  } catch (err) {
42
- setError(err instanceof Error ? err.message : "Failed to load report versions");
43
  setVersions([]);
44
  setSelectedVersion(undefined);
 
 
 
 
45
  } finally {
46
  setLoadingVersions(false);
47
  }
@@ -50,8 +53,11 @@ export function ReportSidebar({ analysis, userId, onCollapse }: ReportSidebarPro
50
  useEffect(() => {
51
  setDetail(null);
52
  setPrecondition(null);
53
- if (analysis?.id) loadVersions(analysis.id);
54
- else setVersions([]);
 
 
 
55
  }, [analysis?.id]);
56
 
57
  useEffect(() => {
@@ -78,7 +84,7 @@ export function ReportSidebar({ analysis, userId, onCollapse }: ReportSidebarPro
78
  try {
79
  const report = await generateReport(analysis.id, userId);
80
  setDetail(report);
81
- await loadVersions(analysis.id);
82
  setSelectedVersion(report.version);
83
  } catch (err) {
84
  if (err instanceof AgenticApiError && err.status === 409) {
 
30
  const [error, setError] = useState<string | null>(null);
31
  const [precondition, setPrecondition] = useState<string | null>(null);
32
 
33
+ const loadVersions = async (analysisId: string, options: { silent?: boolean } = {}) => {
34
  setLoadingVersions(true);
35
  setError(null);
36
  try {
 
39
  const latest = [...list].sort((a, b) => b.version - a.version)[0];
40
  setSelectedVersion(latest?.version);
41
  } catch (err) {
 
42
  setVersions([]);
43
  setSelectedVersion(undefined);
44
+ const isMissingReportCollection = err instanceof AgenticApiError && err.status === 404;
45
+ if (!options.silent && !isMissingReportCollection) {
46
+ setError(err instanceof Error ? err.message : "Failed to load report versions");
47
+ }
48
  } finally {
49
  setLoadingVersions(false);
50
  }
 
53
  useEffect(() => {
54
  setDetail(null);
55
  setPrecondition(null);
56
+ if (analysis?.id) loadVersions(analysis.id, { silent: true });
57
+ else {
58
+ setVersions([]);
59
+ setSelectedVersion(undefined);
60
+ }
61
  }, [analysis?.id]);
62
 
63
  useEffect(() => {
 
84
  try {
85
  const report = await generateReport(analysis.id, userId);
86
  setDetail(report);
87
+ await loadVersions(analysis.id, { silent: true });
88
  setSelectedVersion(report.version);
89
  } catch (err) {
90
  if (err instanceof AgenticApiError && err.status === 409) {
src/services/agenticApi.ts CHANGED
@@ -158,7 +158,10 @@ async function readEventStream(res: Response, onEvent: (event: AgentStreamEvent)
158
 
159
  for (const line of lines) {
160
  if (line.startsWith("event:")) eventName = line.slice(6).trim();
161
- if (line.startsWith("data:")) dataLines.push(line.slice(5).trimStart());
 
 
 
162
  }
163
 
164
  if (dataLines.length === 0) return;
 
158
 
159
  for (const line of lines) {
160
  if (line.startsWith("event:")) eventName = line.slice(6).trim();
161
+ if (line.startsWith("data:")) {
162
+ const value = line.startsWith("data: ") ? line.slice(6) : line.slice(5);
163
+ dataLines.push(value);
164
+ }
165
  }
166
 
167
  if (dataLines.length === 0) return;
src/styles/theme.css CHANGED
@@ -263,6 +263,38 @@
263
  background-color: #1e293b !important;
264
  }
265
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
266
  .dark .workspace-shell input,
267
  .dark .workspace-shell textarea,
268
  .dark .workspace-shell select,
 
263
  background-color: #1e293b !important;
264
  }
265
 
266
+ .dark .workspace-shell .bg-emerald-50,
267
+ .dark .workspace-shell .bg-emerald-50\/60,
268
+ .dark .knowledge-surface .bg-emerald-50,
269
+ .dark .knowledge-surface .bg-emerald-50\/60,
270
+ .dark .workspace-shell .hover\:bg-emerald-50:hover,
271
+ .dark .knowledge-surface .hover\:bg-emerald-50:hover {
272
+ background-color: #064e3b !important;
273
+ }
274
+
275
+ .dark .workspace-shell .bg-emerald-100,
276
+ .dark .workspace-shell .hover\:bg-emerald-100:hover,
277
+ .dark .knowledge-surface .bg-emerald-100,
278
+ .dark .knowledge-surface .hover\:bg-emerald-100:hover {
279
+ background-color: #065f46 !important;
280
+ }
281
+
282
+ .dark .workspace-shell .border-emerald-100,
283
+ .dark .workspace-shell .border-emerald-200,
284
+ .dark .knowledge-surface .border-emerald-100,
285
+ .dark .knowledge-surface .border-emerald-200 {
286
+ border-color: #047857 !important;
287
+ }
288
+
289
+ .dark .workspace-shell .text-emerald-500,
290
+ .dark .workspace-shell .text-emerald-600,
291
+ .dark .workspace-shell .text-emerald-700,
292
+ .dark .knowledge-surface .text-emerald-500,
293
+ .dark .knowledge-surface .text-emerald-600,
294
+ .dark .knowledge-surface .text-emerald-700 {
295
+ color: #6ee7b7 !important;
296
+ }
297
+
298
  .dark .workspace-shell input,
299
  .dark .workspace-shell textarea,
300
  .dark .workspace-shell select,