Commit bc316911 authored by Kantz's avatar Kantz
Browse files

plain text und render für Nachrichten

parent cd16d7c1
import { useEffect, useRef } from "react"; import { useEffect, useRef, useState } from "react";
import ReactMarkdown, { defaultUrlTransform } from "react-markdown"; import ReactMarkdown, { defaultUrlTransform } from "react-markdown";
import type { RetrievedDoc } from "../Retrieval/DocPanel"; import type { RetrievedDoc } from "../Retrieval/DocPanel";
import { t } from "../../i18n"; import { t } from "../../i18n";
...@@ -27,8 +27,13 @@ export default function MessageBubble({ ...@@ -27,8 +27,13 @@ export default function MessageBubble({
docSlugIndex, docSlugIndex,
}: MessageBubbleProps) { }: MessageBubbleProps) {
const bubbleRef = useRef<HTMLDivElement | null>(null); const bubbleRef = useRef<HTMLDivElement | null>(null);
const [isRawView, setIsRawView] = useState(false);
useEffect(() => { useEffect(() => {
if (isRawView) {
return;
}
const typeset = () => { const typeset = () => {
if (window.MathJax?.typesetPromise && bubbleRef.current) { if (window.MathJax?.typesetPromise && bubbleRef.current) {
window.MathJax.typesetPromise([bubbleRef.current]).catch(() => undefined); window.MathJax.typesetPromise([bubbleRef.current]).catch(() => undefined);
...@@ -45,32 +50,75 @@ export default function MessageBubble({ ...@@ -45,32 +50,75 @@ export default function MessageBubble({
script.addEventListener("load", typeset); script.addEventListener("load", typeset);
return () => script.removeEventListener("load", typeset); return () => script.removeEventListener("load", typeset);
} }
}, [text]); }, [text, isRawView]);
return ( return (
<div className={`message-bubble ${role}`} ref={bubbleRef}> <div className={`message-bubble ${role}`} ref={bubbleRef}>
<div className="message-role"> <div className="message-header">
{role === "user" ? t("roleUser") : t("roleAssistant")} <div className="message-role">
{role === "user" ? t("roleUser") : t("roleAssistant")}
</div>
<button
type="button"
className="message-toggle"
aria-pressed={isRawView}
title={isRawView ? t("showRendered") : t("showSource")}
onClick={() => setIsRawView((prev) => !prev)}
>
{isRawView ? t("showRendered") : t("showSource")}
</button>
</div> </div>
<div className="message-text"> <div className="message-text">
<ReactMarkdown {isRawView ? (
urlTransform={(url) => { <pre className="message-raw-text">{text}</pre>
if (url.startsWith("doc://")) { ) : (
return url; <ReactMarkdown
} urlTransform={(url) => {
return defaultUrlTransform(url); if (url.startsWith("doc://")) {
}} return url;
components={{ }
a: ({ href, children, ...props }) => { return defaultUrlTransform(url);
const target = href || ""; }}
if (!target.startsWith("doc://")) { components={{
const mdMatch = target.match(/([^/]+)\.md$/i); a: ({ href, children, ...props }) => {
if (!mdMatch) { const target = href || "";
if (!target.startsWith("doc://")) {
const mdMatch = target.match(/([^/]+)\.md$/i);
if (!mdMatch) {
return (
<a
href={href}
target="_blank"
rel="noreferrer"
{...props}
>
{children}
</a>
);
}
const file = mdMatch[1] || "";
const slugFromFile = file
.toLowerCase()
.replace(/ä/g, "ae")
.replace(/ö/g, "oe")
.replace(/ü/g, "ue")
.replace(/ß/g, "ss")
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
const slug = slugFromFile.split("-").pop() || slugFromFile;
const doc = docSlugIndex ? docSlugIndex[slug] : undefined;
return ( return (
<a <a
href={href} href={href}
target="_blank" onClick={(event) => {
rel="noreferrer" if (!doc || !onInspectDoc) {
return;
}
event.preventDefault();
onInspectDoc(doc);
}}
{...props} {...props}
> >
{children} {children}
...@@ -78,18 +126,8 @@ export default function MessageBubble({ ...@@ -78,18 +126,8 @@ export default function MessageBubble({
); );
} }
const file = mdMatch[1] || ""; const key = decodeURIComponent(target.slice("doc://".length));
const slugFromFile = file const doc = docIndex ? docIndex[key] : undefined;
.toLowerCase()
.replace(/ä/g, "ae")
.replace(/ö/g, "oe")
.replace(/ü/g, "ue")
.replace(/ß/g, "ss")
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
const slug = slugFromFile.split("-").pop() || slugFromFile;
const doc = docSlugIndex ? docSlugIndex[slug] : undefined;
return ( return (
<a <a
href={href} href={href}
...@@ -105,30 +143,12 @@ export default function MessageBubble({ ...@@ -105,30 +143,12 @@ export default function MessageBubble({
{children} {children}
</a> </a>
); );
} },
}}
const key = decodeURIComponent(target.slice("doc://".length)); >
const doc = docIndex ? docIndex[key] : undefined; {text}
return ( </ReactMarkdown>
<a )}
href={href}
onClick={(event) => {
if (!doc || !onInspectDoc) {
return;
}
event.preventDefault();
onInspectDoc(doc);
}}
{...props}
>
{children}
</a>
);
},
}}
>
{text}
</ReactMarkdown>
</div> </div>
</div> </div>
); );
......
...@@ -36,6 +36,8 @@ ...@@ -36,6 +36,8 @@
send: "Send", send: "Send",
roleUser: "user", roleUser: "user",
roleAssistant: "assistant", roleAssistant: "assistant",
showSource: "Show source",
showRendered: "Show rendered",
noTasksAvailable: "No tasks available", noTasksAvailable: "No tasks available",
noTaskSelected: "No task selected.", noTaskSelected: "No task selected.",
savedChats: "Saved Chats", savedChats: "Saved Chats",
...@@ -107,6 +109,8 @@ ...@@ -107,6 +109,8 @@
send: "Senden", send: "Senden",
roleUser: "nutzer", roleUser: "nutzer",
roleAssistant: "assistent", roleAssistant: "assistent",
showSource: "Formeltext anzeigen",
showRendered: "Gerendert anzeigen",
noTasksAvailable: "Keine Aufgaben verfügbar", noTasksAvailable: "Keine Aufgaben verfügbar",
noTaskSelected: "Keine Aufgabe ausgewählt.", noTaskSelected: "Keine Aufgabe ausgewählt.",
savedChats: "Gespeicherte Chats", savedChats: "Gespeicherte Chats",
......
...@@ -420,6 +420,44 @@ body { ...@@ -420,6 +420,44 @@ body {
color: #6f675d; color: #6f675d;
} }
.message-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.message-toggle {
border: 1px solid #d8d1c4;
background: #fef9f0;
color: #6f675d;
border-radius: 999px;
padding: 3px 8px;
font-size: 11px;
cursor: pointer;
}
.message-toggle:hover {
background: #f6f0e4;
}
.message-text {
margin-top: 8px;
}
.message-raw-text {
margin: 0;
padding: 10px;
border-radius: 10px;
border: 1px solid #e2ded5;
background: #fffdf8;
font-family: "Fira Code", "Consolas", "Courier New", monospace;
font-size: 13px;
line-height: 1.45;
white-space: pre-wrap;
overflow-wrap: anywhere;
}
.composer { .composer {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment