Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in
Toggle navigation
Menu
Open sidebar
math_tutor_dev
public_math_tutor
Commits
bc316911
Commit
bc316911
authored
Mar 05, 2026
by
Kantz
Browse files
plain text und render für Nachrichten
parent
cd16d7c1
Changes
3
Hide whitespace changes
Inline
Side-by-side
math-tutor/frontend/src/components/Chat/MessageBubble.tsx
View file @
bc316911
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
>
);
);
...
...
math-tutor/frontend/src/i18n.ts
View file @
bc316911
...
@@ -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
"
,
...
...
math-tutor/frontend/src/styles/theme.css
View file @
bc316911
...
@@ -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
;
...
...
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment