Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
K
KotGPT
Overview
Overview
Details
Activity
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
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Michael Pastushkov
KotGPT
Commits
9868b282
Commit
9868b282
authored
Aug 17, 2025
by
Michael Pastushkov
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix
parent
7bd1e40b
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
133 additions
and
11 deletions
+133
-11
static/www/index.html
+133
-11
static/www/index4.html
+0
-0
No files found.
static/www/index.html
View file @
9868b282
...
@@ -11,12 +11,51 @@
...
@@ -11,12 +11,51 @@
display
:
flex
;
display
:
flex
;
flex-direction
:
column
;
flex-direction
:
column
;
background-color
:
#f8f9fa
;
background-color
:
#f8f9fa
;
margin
:
0
;
}
}
main
{
flex
:
1
1
auto
;
overflow-y
:
auto
;
position
:
relative
;
}
header
{
position
:
sticky
;
top
:
0
;
z-index
:
1020
;
}
.composer
{
position
:
sticky
;
bottom
:
0
;
z-index
:
1020
;
background
:
#fff
;
border-top
:
1px
solid
#dee2e6
;
}
footer
{
font-size
:
.75rem
;
color
:
#6c757d
;
text-align
:
center
;
padding
:
.5rem
;
border-top
:
1px
solid
#dee2e6
;
background
:
#fdfdfd
;
}
/* Make only main scrollable, with space for header+composer+footer */
main
{
flex
:
1
1
auto
;
overflow-y
:
auto
;
position
:
relative
;
padding
:
5rem
0
8rem
;
/* top space for header, bottom space for composer+footer */
}
/* Header fixed at top */
header
{
position
:
fixed
;
top
:
0
;
left
:
0
;
width
:
100%
;
z-index
:
1020
;
}
/* Composer fixed above footer */
.composer
{
position
:
fixed
;
bottom
:
2rem
;
/* leave space for footer */
left
:
0
;
width
:
100%
;
z-index
:
1020
;
background
:
#fff
;
border-top
:
1px
solid
#dee2e6
;
}
/* Footer fixed at very bottom */
footer
{
position
:
fixed
;
bottom
:
0
;
left
:
0
;
width
:
100%
;
z-index
:
1020
;
font-size
:
.75rem
;
color
:
#6c757d
;
text-align
:
center
;
padding
:
.5rem
;
border-top
:
1px
solid
#dee2e6
;
background
:
#fdfdfd
;
}
.cat-spinner
{
width
:
36px
;
height
:
36px
;
object-fit
:
contain
;
filter
:
drop-shadow
(
0
1px
2px
rgba
(
0
,
0
,
0
,
.35
));
}
.cat-spinner
{
width
:
36px
;
height
:
36px
;
object-fit
:
contain
;
filter
:
drop-shadow
(
0
1px
2px
rgba
(
0
,
0
,
0
,
.35
));
}
.thinking-cat
{
width
:
64px
;
height
:
64px
;
object-fit
:
contain
;
display
:
block
;
margin
:
.25rem
auto
;
}
.thinking-cat
{
width
:
64px
;
height
:
64px
;
object-fit
:
contain
;
display
:
block
;
margin
:
.25rem
auto
;
}
.message
{
.message
{
...
@@ -52,6 +91,35 @@
...
@@ -52,6 +91,35 @@
filter
:
drop-shadow
(
0
6px
16px
rgba
(
0
,
0
,
0
,
.15
));
filter
:
drop-shadow
(
0
6px
16px
rgba
(
0
,
0
,
0
,
.15
));
opacity
:
.98
;
opacity
:
.98
;
}
}
/* --- Floating Scroll Button --- */
.scroll-fab
{
position
:
fixed
;
right
:
24px
;
bottom
:
96px
;
/* sits above the sticky composer */
width
:
48px
;
height
:
48px
;
border-radius
:
50%
;
border
:
none
;
background
:
#0d6efd
;
color
:
#fff
;
display
:
none
;
/* toggled via JS */
align-items
:
center
;
justify-content
:
center
;
box-shadow
:
0
8px
18px
rgba
(
13
,
110
,
253
,
.35
);
cursor
:
pointer
;
z-index
:
1030
;
}
.scroll-fab
:focus
{
outline
:
none
;
box-shadow
:
0
0
0
0.25rem
rgba
(
13
,
110
,
253
,
.25
);
}
.scroll-fab
svg
{
width
:
22px
;
height
:
22px
;
pointer-events
:
none
;
}
.scroll-fab.pulse
{
animation
:
scrollPulse
1.5s
ease-in-out
infinite
;
}
@keyframes
scrollPulse
{
0
%
{
transform
:
translateY
(
0
);
}
50
%
{
transform
:
translateY
(
2px
);
}
100
%
{
transform
:
translateY
(
0
);
}
}
</style>
</style>
</head>
</head>
<body>
<body>
...
@@ -73,6 +141,14 @@
...
@@ -73,6 +141,14 @@
<div
id=
"thread"
></div>
<div
id=
"thread"
></div>
</main>
</main>
<!-- Floating Scroll-to-Bottom Button -->
<button
id=
"scrollFab"
class=
"scroll-fab"
type=
"button"
aria-label=
""
>
<!-- Down arrow icon -->
<svg
viewBox=
"0 0 24 24"
fill=
"currentColor"
aria-hidden=
"true"
>
<path
d=
"M12 16a1 1 0 0 1-.7-.29l-7-7a1 1 0 1 1 1.4-1.42L12 13.59l6.3-6.3a1 1 0 0 1 1.4 1.42l-7 7A1 1 0 0 1 12 16z"
/>
</svg>
</button>
<!-- Composer -->
<!-- Composer -->
<div
class=
"composer py-3"
>
<div
class=
"composer py-3"
>
<div
class=
"container"
>
<div
class=
"container"
>
...
@@ -87,9 +163,7 @@
...
@@ -87,9 +163,7 @@
</div>
</div>
<!-- Disclaimer Footer -->
<!-- Disclaimer Footer -->
<footer
id=
"disclaimer"
>
<footer
id=
"disclaimer"
></footer>
<!-- text filled by i18n -->
</footer>
<script
src=
"https://cdn.jsdelivr.net/npm/marked@12.0.2/marked.min.js"
></script>
<script
src=
"https://cdn.jsdelivr.net/npm/marked@12.0.2/marked.min.js"
></script>
<script
src=
"https://cdn.jsdelivr.net/npm/dompurify@3.1.6/dist/purify.min.js"
></script>
<script
src=
"https://cdn.jsdelivr.net/npm/dompurify@3.1.6/dist/purify.min.js"
></script>
...
@@ -160,6 +234,11 @@
...
@@ -160,6 +234,11 @@
if
(
stopBtn
)
stopBtn
.
textContent
=
t
(
'buttons.stop'
,
'Stop'
);
if
(
stopBtn
)
stopBtn
.
textContent
=
t
(
'buttons.stop'
,
'Stop'
);
const
disclaimer
=
document
.
getElementById
(
'disclaimer'
);
const
disclaimer
=
document
.
getElementById
(
'disclaimer'
);
if
(
disclaimer
)
disclaimer
.
textContent
=
t
(
'footer.disclaimer'
,
'Beware: Cat can make mistakes.'
);
if
(
disclaimer
)
disclaimer
.
textContent
=
t
(
'footer.disclaimer'
,
'Beware: Cat can make mistakes.'
);
const
scrollFab
=
document
.
getElementById
(
'scrollFab'
);
if
(
scrollFab
)
{
scrollFab
.
setAttribute
(
'aria-label'
,
t
(
'buttons.scrollDown'
,
'Scroll to latest'
));
scrollFab
.
title
=
t
(
'buttons.scrollDown'
,
'Scroll to latest'
);
}
setWelcomeImage
();
setWelcomeImage
();
}
}
...
@@ -173,18 +252,43 @@
...
@@ -173,18 +252,43 @@
const
goBtn
=
document
.
getElementById
(
'goBtn'
);
const
goBtn
=
document
.
getElementById
(
'goBtn'
);
const
stopBtn
=
document
.
getElementById
(
'stopBtn'
);
const
stopBtn
=
document
.
getElementById
(
'stopBtn'
);
const
welcomeLogo
=
document
.
getElementById
(
'welcomeLogo'
);
const
welcomeLogo
=
document
.
getElementById
(
'welcomeLogo'
);
const
mainScroll
=
document
.
getElementById
(
'mainScroll'
);
const
scrollFab
=
document
.
getElementById
(
'scrollFab'
);
let
firstRequestDone
=
false
;
let
firstRequestDone
=
false
;
let
controller
=
null
;
let
controller
=
null
;
// --- Floating Scroll Button logic ---
const
BOTTOM_EPS
=
32
;
// pixels from bottom considered "at bottom"
function
isOverflowing
()
{
return
mainScroll
.
scrollHeight
-
mainScroll
.
clientHeight
>
1
;
}
function
isNearBottom
()
{
return
(
mainScroll
.
scrollHeight
-
mainScroll
.
clientHeight
-
mainScroll
.
scrollTop
)
<=
BOTTOM_EPS
;
}
function
updateScrollFabVisibility
()
{
if
(
isOverflowing
()
&&
!
isNearBottom
())
{
scrollFab
.
style
.
display
=
'flex'
;
}
else
{
scrollFab
.
style
.
display
=
'none'
;
scrollFab
.
classList
.
remove
(
'pulse'
);
}
}
function
scrollToBottom
(
smooth
=
true
)
{
mainScroll
.
scrollTo
({
top
:
mainScroll
.
scrollHeight
,
behavior
:
smooth
?
'smooth'
:
'auto'
});
}
scrollFab
.
addEventListener
(
'click'
,
()
=>
scrollToBottom
(
true
));
mainScroll
.
addEventListener
(
'scroll'
,
updateScrollFabVisibility
);
window
.
addEventListener
(
'resize'
,
updateScrollFabVisibility
);
window
.
addEventListener
(
'DOMContentLoaded'
,
async
()
=>
{
window
.
addEventListener
(
'DOMContentLoaded'
,
async
()
=>
{
try
{
try
{
await
fetch
(
'/api/clear'
,
{
method
:
'GET'
});
await
fetch
(
'/api/clear'
,
{
method
:
'GET'
});
}
catch
(
err
)
{
}
catch
(
err
)
{
/* ignore */
}
// nothing to do
}
await
loadLocale
(
CURRENT_LOCALE
);
await
loadLocale
(
CURRENT_LOCALE
);
inputText
.
focus
();
inputText
.
focus
();
updateScrollFabVisibility
();
});
});
function
renderMarkdown
(
targetEl
,
markdownText
)
{
function
renderMarkdown
(
targetEl
,
markdownText
)
{
...
@@ -233,6 +337,10 @@
...
@@ -233,6 +337,10 @@
inputText
.
value
=
''
;
inputText
.
value
=
''
;
controller
=
new
AbortController
();
controller
=
new
AbortController
();
setComposerLocked
(
true
);
setComposerLocked
(
true
);
// ensure we evaluate FAB after new nodes
updateScrollFabVisibility
();
try
{
try
{
const
res
=
await
fetch
(
ENDPOINT
,
{
const
res
=
await
fetch
(
ENDPOINT
,
{
method
:
'POST'
,
method
:
'POST'
,
...
@@ -246,6 +354,7 @@
...
@@ -246,6 +354,7 @@
let
rawMarkdown
=
''
;
let
rawMarkdown
=
''
;
let
receivedAny
=
false
;
let
receivedAny
=
false
;
let
rafPending
=
false
;
let
rafPending
=
false
;
const
flush
=
()
=>
{
const
flush
=
()
=>
{
if
(
rafPending
)
return
;
if
(
rafPending
)
return
;
rafPending
=
true
;
rafPending
=
true
;
...
@@ -253,13 +362,25 @@
...
@@ -253,13 +362,25 @@
rafPending
=
false
;
rafPending
=
false
;
if
(
!
receivedAny
)
{
clearThinkingCatIfPresent
(
assistantMsg
);
receivedAny
=
true
;
}
if
(
!
receivedAny
)
{
clearThinkingCatIfPresent
(
assistantMsg
);
receivedAny
=
true
;
}
renderMarkdown
(
assistantMsg
,
rawMarkdown
);
renderMarkdown
(
assistantMsg
,
rawMarkdown
);
// Auto-scroll only if user is already near the bottom.
// If not, show FAB and pulse to indicate new content.
if
(
isNearBottom
())
{
scrollToBottom
(
true
);
}
else
{
scrollFab
.
style
.
display
=
'flex'
;
scrollFab
.
classList
.
add
(
'pulse'
);
}
updateScrollFabVisibility
();
});
});
};
};
for
(;;)
{
for
(;;)
{
const
{
value
,
done
}
=
await
reader
.
read
();
const
{
value
,
done
}
=
await
reader
.
read
();
if
(
done
)
break
;
if
(
done
)
break
;
const
chunk
=
decoder
.
decode
(
value
,
{
stream
:
true
});
const
chunk
=
decoder
.
decode
(
value
,
{
stream
:
true
});
for
(
const
ch
of
chunk
)
{
rawMarkdown
+=
ch
;
flush
();
}
for
(
const
ch
of
chunk
)
{
rawMarkdown
+=
ch
;
}
flush
();
}
}
flush
();
flush
();
}
catch
(
err
)
{
}
catch
(
err
)
{
...
@@ -275,6 +396,7 @@
...
@@ -275,6 +396,7 @@
}
finally
{
}
finally
{
setComposerLocked
(
false
);
setComposerLocked
(
false
);
controller
=
null
;
controller
=
null
;
updateScrollFabVisibility
();
}
}
}
}
...
...
static/www/index
copy
4.html
→
static/www/index4.html
View file @
9868b282
File moved
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