Commit 8cedace3 authored by Kantz's avatar Kantz
Browse files

thinkging hinter einem Button

parent a242bdc4
...@@ -29,54 +29,32 @@ export default function MessageBubble({ ...@@ -29,54 +29,32 @@ export default function MessageBubble({
}: MessageBubbleProps) { }: MessageBubbleProps) {
const bubbleRef = useRef<HTMLDivElement | null>(null); const bubbleRef = useRef<HTMLDivElement | null>(null);
const [isRawView, setIsRawView] = useState(false); const [isRawView, setIsRawView] = useState(false);
const renderedText = useMemo( const [isThinkingExpanded, setIsThinkingExpanded] = useState(false);
() => escapeAsterisksInsideMath(text.replaceAll("</think>", "\n --- \n")),
[text]
);
useEffect(() => { const { thinkingText, answerText } = useMemo(() => {
if (isRawView) { const parts = text.split("</think>");
return; if (role !== "assistant" || parts.length < 2) {
return {
thinkingText: "",
answerText: escapeAsterisksInsideMath(text),
};
} }
const typeset = () => { const rawThinking = parts.slice(0, -1).join("</think>").trim();
if (window.MathJax?.typesetPromise && bubbleRef.current) { const rawAnswer = parts[parts.length - 1].trim();
window.MathJax.typesetPromise([bubbleRef.current]).catch(() => undefined); return {
} thinkingText: escapeAsterisksInsideMath(rawThinking),
answerText: escapeAsterisksInsideMath(rawAnswer),
}; };
}, [role, text]);
if (window.MathJax?.typesetPromise) { const hasThinking = Boolean(thinkingText);
typeset();
return;
}
const script = document.getElementById("mathjax-script"); useEffect(() => {
if (script) { setIsThinkingExpanded(false);
script.addEventListener("load", typeset); }, [text]);
return () => script.removeEventListener("load", typeset);
}
}, [text, isRawView]);
return ( const renderMarkdown = (value: string) => (
<div className={`message-bubble ${role}`} ref={bubbleRef}>
<div className="message-header">
<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 className="message-text">
{isRawView ? (
<pre className="message-raw-text">{text}</pre>
) : (
<ReactMarkdown <ReactMarkdown
urlTransform={(url) => { urlTransform={(url) => {
if (url.startsWith("doc://")) { if (url.startsWith("doc://")) {
...@@ -91,12 +69,7 @@ export default function MessageBubble({ ...@@ -91,12 +69,7 @@ export default function MessageBubble({
const mdMatch = target.match(/([^/]+)\.md$/i); const mdMatch = target.match(/([^/]+)\.md$/i);
if (!mdMatch) { if (!mdMatch) {
return ( return (
<a <a href={href} target="_blank" rel="noreferrer" {...props}>
href={href}
target="_blank"
rel="noreferrer"
{...props}
>
{children} {children}
</a> </a>
); );
...@@ -105,10 +78,10 @@ export default function MessageBubble({ ...@@ -105,10 +78,10 @@ export default function MessageBubble({
const file = mdMatch[1] || ""; const file = mdMatch[1] || "";
const slugFromFile = file const slugFromFile = file
.toLowerCase() .toLowerCase()
.replace(/ä/g, "ae") .replace(/ä/g, "ae")
.replace(/ö/g, "oe") .replace(/ö/g, "oe")
.replace(/ü/g, "ue") .replace(/ü/g, "ue")
.replace(/ß/g, "ss") .replace(/ß/g, "ss")
.replace(/[^a-z0-9]+/g, "-") .replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, ""); .replace(/^-+|-+$/g, "");
const slug = slugFromFile.split("-").pop() || slugFromFile; const slug = slugFromFile.split("-").pop() || slugFromFile;
...@@ -151,8 +124,71 @@ export default function MessageBubble({ ...@@ -151,8 +124,71 @@ export default function MessageBubble({
}, },
}} }}
> >
{renderedText} {value}
</ReactMarkdown> </ReactMarkdown>
);
useEffect(() => {
if (isRawView) {
return;
}
const typeset = () => {
if (window.MathJax?.typesetPromise && bubbleRef.current) {
window.MathJax.typesetPromise([bubbleRef.current]).catch(() => undefined);
}
};
if (window.MathJax?.typesetPromise) {
typeset();
return;
}
const script = document.getElementById("mathjax-script");
if (script) {
script.addEventListener("load", typeset);
return () => script.removeEventListener("load", typeset);
}
}, [answerText, isRawView, isThinkingExpanded, thinkingText]);
return (
<div className={`message-bubble ${role}`} ref={bubbleRef}>
<div className="message-header">
<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 className="message-text">
{isRawView ? (
<pre className="message-raw-text">{text}</pre>
) : (
<>
{hasThinking ? (
<div className="message-thinking">
<button
type="button"
className="message-thinking-toggle"
aria-expanded={isThinkingExpanded}
onClick={() => setIsThinkingExpanded((prev) => !prev)}
>
{isThinkingExpanded ? t("hideThinking") : t("showThinking")}
</button>
{isThinkingExpanded ? (
<div className="message-thinking-content">{renderMarkdown(thinkingText)}</div>
) : null}
</div>
) : null}
<div className="message-answer">{renderMarkdown(answerText)}</div>
</>
)} )}
</div> </div>
</div> </div>
......
...@@ -40,6 +40,8 @@ ...@@ -40,6 +40,8 @@
roleAssistant: "assistant", roleAssistant: "assistant",
showSource: "Show source", showSource: "Show source",
showRendered: "Show rendered", showRendered: "Show rendered",
showThinking: "Show thinking",
hideThinking: "Hide thinking",
noTasksAvailable: "No tasks available", noTasksAvailable: "No tasks available",
noTaskSelected: "No task selected.", noTaskSelected: "No task selected.",
savedChats: "Saved Chats", savedChats: "Saved Chats",
...@@ -117,6 +119,8 @@ ...@@ -117,6 +119,8 @@
roleAssistant: "assistent", roleAssistant: "assistent",
showSource: "Formeltext anzeigen", showSource: "Formeltext anzeigen",
showRendered: "Gerendert anzeigen", showRendered: "Gerendert anzeigen",
showThinking: "Thinking anzeigen",
hideThinking: "Thinking ausblenden",
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",
......
...@@ -466,6 +466,34 @@ body { ...@@ -466,6 +466,34 @@ body {
overflow-wrap: anywhere; overflow-wrap: anywhere;
} }
.message-thinking {
margin-bottom: 12px;
padding-bottom: 12px;
border-bottom: 1px dashed #d8d1c4;
}
.message-thinking-toggle {
border: 1px solid #d8d1c4;
background: #f6f0e4;
color: #6f675d;
border-radius: 999px;
padding: 4px 10px;
font-size: 11px;
cursor: pointer;
}
.message-thinking-toggle:hover {
background: #efe6d4;
}
.message-thinking-content {
margin-top: 10px;
padding: 10px 12px;
border-radius: 10px;
background: #fffaf0;
border: 1px solid #e2ded5;
}
.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