Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Q
qjclaw-dmg
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
AI-甘富林
qjclaw-dmg
Commits
0355027e
Commit
0355027e
authored
May 12, 2026
by
edy
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat(ui): show streaming wait state as reasoning trace
parent
362f9163
Changes
7
Show whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
149 additions
and
45 deletions
+149
-45
App.tsx
apps/ui/src/App.tsx
+0
-1
appCopy.ts
apps/ui/src/features/app/appCopy.ts
+0
-2
ConversationWorkspaceView.tsx
apps/ui/src/features/chat/ConversationWorkspaceView.tsx
+0
-1
MessageList.tsx
apps/ui/src/features/chat/MessageList.tsx
+19
-27
messageTraceDisplay.ts
apps/ui/src/features/chat/messageTraceDisplay.ts
+29
-0
components.css
apps/ui/src/styles/components.css
+89
-13
theme-openclaw.css
apps/ui/src/styles/theme-openclaw.css
+12
-1
No files found.
apps/ui/src/App.tsx
View file @
0355027e
...
@@ -1203,7 +1203,6 @@ export default function App() {
...
@@ -1203,7 +1203,6 @@ export default function App() {
sending
,
sending
,
messageLabels
:
{
messageLabels
:
{
thinking
:
ui
.
thinking
,
thinking
:
ui
.
thinking
,
traceTitle
:
ui
.
traceTitle
,
hideTrace
:
ui
.
hideTrace
,
hideTrace
:
ui
.
hideTrace
,
traceCollapsed
:
ui
.
traceCollapsed
traceCollapsed
:
ui
.
traceCollapsed
},
},
...
...
apps/ui/src/features/app/appCopy.ts
View file @
0355027e
...
@@ -62,7 +62,6 @@ export const ui = {
...
@@ -62,7 +62,6 @@ export const ui = {
projectSessionsLabel
:
"
\
u4f1a
\
u8bdd"
,
projectSessionsLabel
:
"
\
u4f1a
\
u8bdd"
,
newSession
:
"
\
u65b0
\
u5efa
\
u4f1a
\
u8bdd"
,
newSession
:
"
\
u65b0
\
u5efa
\
u4f1a
\
u8bdd"
,
closeSession
:
"
\
u5173
\
u95ed
\
u4f1a
\
u8bdd"
,
closeSession
:
"
\
u5173
\
u95ed
\
u4f1a
\
u8bdd"
,
traceTitle
:
"
\
u601d
\
u8003
\
u8fc7
\
u7a0b"
,
hideTrace
:
"
\
u6536
\
u8d77"
,
hideTrace
:
"
\
u6536
\
u8d77"
,
traceEmpty
:
"
\
u8fd8
\
u6ca1
\
u6709
\
u53ef
\
u663e
\
u793a
\
u7684
\
u8fdb
\
u5ea6
\
u3002"
,
traceEmpty
:
"
\
u8fd8
\
u6ca1
\
u6709
\
u53ef
\
u663e
\
u793a
\
u7684
\
u8fdb
\
u5ea6
\
u3002"
,
traceCollapsed
:
"
\
u5c55
\
u5f00"
,
traceCollapsed
:
"
\
u5c55
\
u5f00"
,
...
@@ -130,4 +129,3 @@ export const expertsPageCopy = {
...
@@ -130,4 +129,3 @@ export const expertsPageCopy = {
noExperts
:
"当前还没有可用专家,先在首页直接对话即可。"
noExperts
:
"当前还没有可用专家,先在首页直接对话即可。"
}
as
const
;
}
as
const
;
apps/ui/src/features/chat/ConversationWorkspaceView.tsx
View file @
0355027e
...
@@ -76,7 +76,6 @@ interface ConversationWorkspaceViewProps {
...
@@ -76,7 +76,6 @@ interface ConversationWorkspaceViewProps {
sending
:
boolean
sending
:
boolean
messageLabels
:
{
messageLabels
:
{
thinking
:
string
thinking
:
string
traceTitle
:
string
hideTrace
:
string
hideTrace
:
string
traceCollapsed
:
string
traceCollapsed
:
string
}
}
...
...
apps/ui/src/features/chat/MessageList.tsx
View file @
0355027e
import
type
{
ChatAttachment
,
ChatMessage
}
from
"@qjclaw/shared-types"
import
type
{
ChatAttachment
,
ChatMessage
}
from
"@qjclaw/shared-types"
import
{
useEffect
,
useMemo
,
useState
,
type
ReactNode
,
type
RefObject
,
type
UIEvent
}
from
"react"
import
{
useEffect
,
useMemo
,
useState
,
type
ReactNode
,
type
RefObject
,
type
UIEvent
}
from
"react"
import
{
desktopApi
}
from
"../../lib/desktop-api"
import
{
desktopApi
}
from
"../../lib/desktop-api"
import
{
getTraceDisplayLines
,
getTraceLineClassName
}
from
"./messageTraceDisplay"
import
{
getTraceDisplayLines
,
getTraceLineClassName
,
getTraceStripTitle
}
from
"./messageTraceDisplay"
import
type
{
MessageTraceState
}
from
"./useMessageTraces"
import
type
{
MessageTraceState
}
from
"./useMessageTraces"
type
ViewMode
=
"chat"
|
"experts"
|
"plugins"
|
"settings"
|
"knowledge"
type
ViewMode
=
"chat"
|
"experts"
|
"plugins"
|
"settings"
|
"knowledge"
...
@@ -22,7 +22,6 @@ interface VideoStatusCardContent {
...
@@ -22,7 +22,6 @@ interface VideoStatusCardContent {
interface
MessageListLabels
{
interface
MessageListLabels
{
thinking
:
string
thinking
:
string
traceTitle
:
string
hideTrace
:
string
hideTrace
:
string
traceCollapsed
:
string
traceCollapsed
:
string
}
}
...
@@ -204,13 +203,14 @@ export function MessageList({
...
@@ -204,13 +203,14 @@ export function MessageList({
return
(
return
(
<
div
ref=
{
messageListRef
}
onScroll=
{
onScroll
}
className=
{
"message-list chat-scroll-smooth"
+
(
viewMode
===
"chat"
?
" message-list-home"
:
""
)
}
>
<
div
ref=
{
messageListRef
}
onScroll=
{
onScroll
}
className=
{
"message-list chat-scroll-smooth"
+
(
viewMode
===
"chat"
?
" message-list-home"
:
""
)
}
>
{
messages
.
map
((
message
)
=>
{
{
messages
.
map
((
message
)
=>
{
const
showThinking
=
message
.
role
===
"assistant"
&&
message
.
streamState
===
"streaming"
&&
!
message
.
content
.
trim
()
const
isWaitingForContent
=
message
.
role
===
"assistant"
&&
message
.
streamState
===
"streaming"
&&
!
message
.
content
.
trim
()
const
videoStatusCard
=
showThinking
?
buildDouyinVideoStatusCard
(
message
,
activeExpertKey
)
:
null
const
videoStatusCard
=
isWaitingForContent
?
buildDouyinVideoStatusCard
(
message
,
activeExpertKey
)
:
null
const
messageTrace
=
message
.
role
===
"assistant"
?
messageTraces
[
message
.
id
]
:
undefined
const
messageTrace
=
message
.
role
===
"assistant"
?
messageTraces
[
message
.
id
]
:
undefined
const
hasTrace
=
Boolean
(
messageTrace
?.
items
.
length
)
const
hasTrace
=
Boolean
(
messageTrace
?.
items
.
length
)
const
isTraceExpanded
=
Boolean
(
messageTrace
?.
expanded
)
const
isTraceExpanded
=
Boolean
(
messageTrace
?.
expanded
)
const
showReasoningStrip
=
message
.
role
===
"assistant"
&&
!
showThinking
&&
(
hasTrace
||
message
.
streamState
===
"error"
)
const
showReasoningStrip
=
message
.
role
===
"assistant"
&&
!
videoStatusCard
&&
(
hasTrace
||
message
.
streamState
===
"error"
||
isWaitingForContent
)
const
traceDisplayLines
=
hasTrace
?
getTraceDisplayLines
(
messageTrace
?.
items
??
[])
:
[]
const
traceDisplayLines
=
hasTrace
?
getTraceDisplayLines
(
messageTrace
?.
items
??
[])
:
[]
const
traceTitle
=
getTraceStripTitle
(
messageTrace
?.
items
??
[],
labels
.
thinking
,
message
.
statusLabel
,
message
.
statusDetail
)
const
canCopyMessage
=
Boolean
(
message
.
content
.
trim
())
const
canCopyMessage
=
Boolean
(
message
.
content
.
trim
())
const
copyToken
=
`message:${message.id}`
const
copyToken
=
`message:${message.id}`
const
reaction
=
messageReactions
[
message
.
id
]
const
reaction
=
messageReactions
[
message
.
id
]
...
@@ -220,15 +220,15 @@ export function MessageList({
...
@@ -220,15 +220,15 @@ export function MessageList({
<
article
key=
{
message
.
id
}
className=
{
"message-card group "
+
message
.
role
+
(
message
.
streamState
?
" "
+
message
.
streamState
:
""
)
}
>
<
article
key=
{
message
.
id
}
className=
{
"message-card group "
+
message
.
role
+
(
message
.
streamState
?
" "
+
message
.
streamState
:
""
)
}
>
<
div
className=
{
"message-bubble"
+
(
message
.
role
===
"assistant"
?
" message-bubble-assistant"
:
" message-bubble-user"
)
}
>
<
div
className=
{
"message-bubble"
+
(
message
.
role
===
"assistant"
?
" message-bubble-assistant"
:
" message-bubble-user"
)
}
>
{
showReasoningStrip
?
(
{
showReasoningStrip
?
(
<
div
className=
"message-trace"
>
<
div
className=
{
"message-trace"
+
(
message
.
streamState
===
"streaming"
?
" streaming"
:
""
)
}
aria
-
live=
{
message
.
streamState
===
"streaming"
?
"polite"
:
undefined
}
>
<
button
<
button
type=
"button"
type=
"button"
className=
{
"reasoning-strip"
+
(
isTraceExpanded
?
" expanded"
:
""
)
+
(
message
.
streamState
===
"error"
?
" error"
:
""
)
}
className=
{
"reasoning-strip"
+
(
isTraceExpanded
?
" expanded"
:
""
)
+
(
message
.
streamState
===
"error"
?
" error"
:
""
)
+
(
message
.
streamState
===
"streaming"
?
" streaming"
:
""
)
+
(
isWaitingForContent
?
" waiting"
:
""
)
}
onClick=
{
()
=>
onTraceExpandedChange
(
message
.
id
,
!
isTraceExpanded
)
}
onClick=
{
()
=>
onTraceExpandedChange
(
message
.
id
,
!
isTraceExpanded
)
}
aria
-
expanded=
{
isTraceExpanded
}
aria
-
expanded=
{
isTraceExpanded
}
>
>
<
span
className=
"reasoning-strip-leading"
aria
-
hidden=
"true"
/>
<
span
className=
"reasoning-strip-leading"
aria
-
hidden=
"true"
/>
<
span
className=
"reasoning-strip-title"
>
{
labels
.
traceTitle
}
</
span
>
<
span
className=
"reasoning-strip-title"
>
{
traceTitle
}
</
span
>
<
span
className=
"reasoning-strip-action"
>
{
isTraceExpanded
?
labels
.
hideTrace
:
labels
.
traceCollapsed
}
</
span
>
<
span
className=
"reasoning-strip-action"
>
{
isTraceExpanded
?
labels
.
hideTrace
:
labels
.
traceCollapsed
}
</
span
>
</
button
>
</
button
>
{
isTraceExpanded
?
(
{
isTraceExpanded
?
(
...
@@ -251,8 +251,7 @@ export function MessageList({
...
@@ -251,8 +251,7 @@ export function MessageList({
)
:
null
}
)
:
null
}
</
div
>
</
div
>
)
:
null
}
)
:
null
}
{
showThinking
?
(
{
isWaitingForContent
&&
videoStatusCard
?
(
videoStatusCard
?
(
<
div
className=
"generation-status-card"
aria
-
live=
"polite"
>
<
div
className=
"generation-status-card"
aria
-
live=
"polite"
>
<
div
className=
"generation-status-leading"
>
<
div
className=
"generation-status-leading"
>
<
span
className=
"thinking-spinner generation-status-spinner"
aria
-
hidden=
"true"
/>
<
span
className=
"thinking-spinner generation-status-spinner"
aria
-
hidden=
"true"
/>
...
@@ -265,13 +264,6 @@ export function MessageList({
...
@@ -265,13 +264,6 @@ export function MessageList({
<
span
className=
"generation-status-progress"
aria
-
hidden=
"true"
/>
<
span
className=
"generation-status-progress"
aria
-
hidden=
"true"
/>
</
div
>
</
div
>
</
div
>
</
div
>
)
:
(
<
div
className=
"thinking-indicator"
aria
-
live=
"polite"
>
<
span
className=
"thinking-spinner"
aria
-
hidden=
"true"
/>
<
span
className=
"thinking-label"
>
{
message
.
statusLabel
??
labels
.
thinking
}
</
span
>
{
message
.
statusDetail
?
<
span
className=
"thinking-detail"
>
{
message
.
statusDetail
}
</
span
>
:
null
}
</
div
>
)
)
:
message
.
content
?
(
)
:
message
.
content
?
(
message
.
role
===
"assistant"
?
(
message
.
role
===
"assistant"
?
(
<
div
className=
"markdown-body"
>
<
div
className=
"markdown-body"
>
...
...
apps/ui/src/features/chat/messageTraceDisplay.ts
View file @
0355027e
...
@@ -11,6 +11,8 @@ export interface TraceDisplayLine {
...
@@ -11,6 +11,8 @@ export interface TraceDisplayLine {
kind
:
TraceStepKind
kind
:
TraceStepKind
}
}
const
TRACE_TITLE_MAX_LENGTH
=
34
export
function
formatTraceTime
(
value
:
string
):
string
{
export
function
formatTraceTime
(
value
:
string
):
string
{
const
date
=
new
Date
(
value
)
const
date
=
new
Date
(
value
)
if
(
Number
.
isNaN
(
date
.
getTime
()))
{
if
(
Number
.
isNaN
(
date
.
getTime
()))
{
...
@@ -25,6 +27,33 @@ function compactText(value: string | undefined): string | undefined {
...
@@ -25,6 +27,33 @@ function compactText(value: string | undefined): string | undefined {
return
text
||
undefined
return
text
||
undefined
}
}
function
limitTraceTitle
(
value
:
string
):
string
{
if
(
value
.
length
<=
TRACE_TITLE_MAX_LENGTH
)
{
return
value
}
return
`
${
value
.
slice
(
0
,
TRACE_TITLE_MAX_LENGTH
-
1
)}
...`
}
function
getTraceTitleCandidate
(
item
:
ConversationTraceItem
):
string
|
undefined
{
if
(
item
.
tone
===
"error"
)
{
return
compactText
(
item
.
label
)
}
return
compactText
(
item
.
detail
)
??
compactText
(
item
.
label
)
}
export
function
getTraceStripTitle
(
items
:
ConversationTraceItem
[],
fallbackTitle
:
string
,
statusLabel
?:
string
,
statusDetail
?:
string
):
string
{
const
latestTraceTitle
=
items
.
length
?
getTraceTitleCandidate
(
items
[
items
.
length
-
1
])
:
undefined
const
title
=
latestTraceTitle
??
compactText
(
statusDetail
)
??
compactText
(
statusLabel
)
??
compactText
(
fallbackTitle
)
return
title
?
limitTraceTitle
(
title
)
:
fallbackTitle
}
function
classifyTraceItem
(
item
:
ConversationTraceItem
):
TraceStepKind
{
function
classifyTraceItem
(
item
:
ConversationTraceItem
):
TraceStepKind
{
const
stage
=
item
.
stage
.
toLowerCase
()
const
stage
=
item
.
stage
.
toLowerCase
()
const
value
=
`
${
item
.
stage
}
${
item
.
label
}
`
.
toLowerCase
()
const
value
=
`
${
item
.
stage
}
${
item
.
label
}
`
.
toLowerCase
()
...
...
apps/ui/src/styles/components.css
View file @
0355027e
...
@@ -212,14 +212,6 @@
...
@@ -212,14 +212,6 @@
margin-top
:
6px
;
margin-top
:
6px
;
}
}
.thinking-indicator
{
display
:
inline-flex
;
align-items
:
center
;
gap
:
10px
;
padding-top
:
6px
;
color
:
#46607f
;
}
.thinking-spinner
{
.thinking-spinner
{
width
:
16px
;
width
:
16px
;
height
:
16px
;
height
:
16px
;
...
@@ -229,11 +221,6 @@
...
@@ -229,11 +221,6 @@
animation
:
spinner-rotate
0.8s
linear
infinite
;
animation
:
spinner-rotate
0.8s
linear
infinite
;
}
}
.thinking-label
{
font-size
:
14px
;
font-weight
:
600
;
}
.message-cursor
{
.message-cursor
{
display
:
inline-block
;
display
:
inline-block
;
width
:
8px
;
width
:
8px
;
...
@@ -253,6 +240,9 @@
...
@@ -253,6 +240,9 @@
}
}
.reasoning-strip
{
.reasoning-strip
{
position
:
relative
;
isolation
:
isolate
;
overflow
:
hidden
;
width
:
min
(
100%
,
880px
);
width
:
min
(
100%
,
880px
);
min-height
:
32px
;
min-height
:
32px
;
display
:
grid
;
display
:
grid
;
...
@@ -269,6 +259,11 @@
...
@@ -269,6 +259,11 @@
transition
:
background-color
160ms
ease
,
color
160ms
ease
,
box-shadow
160ms
ease
;
transition
:
background-color
160ms
ease
,
color
160ms
ease
,
box-shadow
160ms
ease
;
}
}
.reasoning-strip
>
span
{
position
:
relative
;
z-index
:
1
;
}
.reasoning-strip
:hover
{
.reasoning-strip
:hover
{
background
:
rgba
(
15
,
23
,
42
,
0.07
);
background
:
rgba
(
15
,
23
,
42
,
0.07
);
color
:
#263244
;
color
:
#263244
;
...
@@ -284,6 +279,31 @@
...
@@ -284,6 +279,31 @@
box-shadow
:
inset
0
0
0
1px
rgba
(
15
,
23
,
42
,
0.035
);
box-shadow
:
inset
0
0
0
1px
rgba
(
15
,
23
,
42
,
0.035
);
}
}
.reasoning-strip.streaming
{
background
:
linear-gradient
(
90deg
,
rgba
(
13
,
148
,
136
,
0.11
),
rgba
(
37
,
99
,
235
,
0.1
),
rgba
(
15
,
23
,
42
,
0.045
));
color
:
#27506d
;
box-shadow
:
inset
0
0
0
1px
rgba
(
59
,
130
,
246
,
0.08
);
}
.reasoning-strip.streaming
::after
{
content
:
""
;
position
:
absolute
;
inset
:
0
;
z-index
:
0
;
background
:
linear-gradient
(
105deg
,
transparent
8%
,
rgba
(
255
,
255
,
255
,
0.42
)
42%
,
transparent
64%
);
opacity
:
0.55
;
transform
:
translateX
(
-115%
);
animation
:
trace-strip-shine
2.4s
ease-in-out
infinite
;
pointer-events
:
none
;
}
.reasoning-strip.streaming
:hover
,
.reasoning-strip.streaming.expanded
{
background
:
linear-gradient
(
90deg
,
rgba
(
13
,
148
,
136
,
0.14
),
rgba
(
37
,
99
,
235
,
0.13
),
rgba
(
15
,
23
,
42
,
0.055
));
}
.reasoning-strip.error
{
.reasoning-strip.error
{
background
:
rgba
(
239
,
68
,
68
,
0.09
);
background
:
rgba
(
239
,
68
,
68
,
0.09
);
color
:
#893535
;
color
:
#893535
;
...
@@ -305,6 +325,12 @@
...
@@ -305,6 +325,12 @@
box-shadow
:
0
0
0
3px
rgba
(
100
,
116
,
139
,
0.12
);
box-shadow
:
0
0
0
3px
rgba
(
100
,
116
,
139
,
0.12
);
}
}
.reasoning-strip.streaming
.reasoning-strip-leading
::before
{
background
:
#0d9488
;
box-shadow
:
0
0
0
3px
rgba
(
13
,
148
,
136
,
0.13
);
animation
:
trace-status-pulse
1.5s
ease-in-out
infinite
;
}
.reasoning-strip.error
.reasoning-strip-leading
::before
{
.reasoning-strip.error
.reasoning-strip-leading
::before
{
background
:
#ef4444
;
background
:
#ef4444
;
box-shadow
:
0
0
0
3px
rgba
(
239
,
68
,
68
,
0.12
);
box-shadow
:
0
0
0
3px
rgba
(
239
,
68
,
68
,
0.12
);
...
@@ -325,6 +351,15 @@
...
@@ -325,6 +351,15 @@
line-height
:
1.45
;
line-height
:
1.45
;
}
}
.reasoning-strip.streaming
.reasoning-strip-title
{
background
:
linear-gradient
(
90deg
,
#0f766e
,
#2563eb
,
#334155
);
background-size
:
180%
100%
;
-webkit-background-clip
:
text
;
background-clip
:
text
;
color
:
transparent
;
animation
:
trace-title-shift
3s
ease-in-out
infinite
;
}
.reasoning-strip-action
{
.reasoning-strip-action
{
color
:
#6f8197
;
color
:
#6f8197
;
font-size
:
12px
;
font-size
:
12px
;
...
@@ -1087,6 +1122,47 @@ select:focus-visible {
...
@@ -1087,6 +1122,47 @@ select:focus-visible {
}
}
}
}
@keyframes
trace-strip-shine
{
0
%
{
transform
:
translateX
(
-115%
);
}
48
%,
100
%
{
transform
:
translateX
(
115%
);
}
}
@keyframes
trace-status-pulse
{
0
%,
100
%
{
opacity
:
0.62
;
transform
:
scale
(
0.92
);
}
50
%
{
opacity
:
1
;
transform
:
scale
(
1.18
);
}
}
@keyframes
trace-title-shift
{
0
%,
100
%
{
background-position
:
0%
50%
;
}
50
%
{
background-position
:
100%
50%
;
}
}
@media
(
prefers-reduced-motion
:
reduce
)
{
.reasoning-strip.streaming
::after
,
.reasoning-strip.streaming
.reasoning-strip-leading
::before
,
.reasoning-strip.streaming
.reasoning-strip-title
{
animation
:
none
;
}
.reasoning-strip.streaming
::after
{
opacity
:
0
;
}
}
@media
(
max-width
:
1100px
)
{
@media
(
max-width
:
1100px
)
{
.hero-line
{
font-size
:
21px
;
}
.hero-line
{
font-size
:
21px
;
}
.composer-footer
{
.composer-footer
{
...
...
apps/ui/src/styles/theme-openclaw.css
View file @
0355027e
...
@@ -572,7 +572,6 @@
...
@@ -572,7 +572,6 @@
line-height
:
1.82
;
line-height
:
1.82
;
}
}
.conversation-shell
.message-card.assistant
.thinking-indicator
,
.conversation-shell
.message-card.assistant
.generation-status-card
,
.conversation-shell
.message-card.assistant
.generation-status-card
,
.conversation-shell
.message-card.assistant
.message-trace
{
.conversation-shell
.message-card.assistant
.message-trace
{
max-width
:
min
(
100%
,
880px
);
max-width
:
min
(
100%
,
880px
);
...
@@ -582,6 +581,18 @@
...
@@ -582,6 +581,18 @@
border-radius
:
22px
;
border-radius
:
22px
;
}
}
.conversation-shell
.message-card.assistant
.message-trace.streaming
{
margin-bottom
:
12px
;
}
.conversation-shell
.message-card.assistant
.reasoning-strip.waiting
{
min-height
:
36px
;
}
.conversation-shell
.message-card.assistant
.reasoning-strip.streaming
.reasoning-strip-action
{
color
:
#64748b
;
}
.conversation-shell
.message-timestamp
{
.conversation-shell
.message-timestamp
{
position
:
absolute
;
position
:
absolute
;
bottom
:
0
;
bottom
:
0
;
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a 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