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
69378d52
Commit
69378d52
authored
Aug 18, 2025
by
Michael Pastushkov
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix
parent
f4230d27
Hide whitespace changes
Inline
Side-by-side
Showing
1 changed file
with
94 additions
and
139 deletions
+94
-139
static/www/1.html
+94
-139
No files found.
static/www/1.html
View file @
69378d52
<!doctype html>
<html
lang=
"en"
>
<head>
<meta
charset=
"utf-8"
/>
<title>
KotGPT
</title>
...
...
@@ -8,14 +7,10 @@
<link
href=
"https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
rel=
"stylesheet"
/>
<style>
:root
{
/* tune these if footer/composer sizes change */
--footer-h
:
2.75rem
;
/* fixed footer height (approx) */
--composer-h
:
6.5rem
;
/* composer block height (approx) */
--footer-h
:
2.75rem
;
/* fixed footer height (approx) */
--composer-h
:
6.5rem
;
/* composer block height (approx) */
--bottom-pad
:
calc
(
var
(
--footer-h
)
+
var
(
--composer-h
)
+
1rem
);
}
body
{
min-height
:
100vh
;
display
:
flex
;
...
...
@@ -23,141 +18,56 @@
background-color
:
#f8f9fa
;
margin
:
0
;
}
header
{
position
:
sticky
;
top
:
0
;
z-index
:
1020
;
}
header
{
position
:
sticky
;
top
:
0
;
z-index
:
1020
;
}
main
{
flex
:
1
1
auto
;
overflow-y
:
auto
;
position
:
relative
;
padding-bottom
:
var
(
--bottom-pad
);
/* leave room for composer + fixed footer
*/
padding-bottom
:
var
(
--bottom-pad
);
/* leave room for composer + fixed footer */
scroll-behavior
:
auto
;
/* avoid jitter during streaming, we’ll smooth when needed
*/
}
.composer
{
position
:
sticky
;
bottom
:
var
(
--footer-h
);
/* sit just above the fixed footer */
bottom
:
var
(
--footer-h
);
/* sit just above the fixed footer */
z-index
:
1020
;
background
:
#fff
;
border-top
:
1px
solid
#dee2e6
;
}
/* --- Fixed, non-scrollable footer --- */
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
;
height
:
var
(
--footer-h
);
box-sizing
:
border-box
;
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
;
height
:
var
(
--footer-h
);
box-sizing
:
border-box
;
}
.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
;
}
.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
;
}
.message
{
max-width
:
900px
;
margin
:
0
auto
1rem
auto
;
padding
:
1rem
1.25rem
;
border-radius
:
.75rem
;
line-height
:
1.6
;
font-size
:
1rem
;
word-break
:
break-word
;
}
.user-message
{
background-color
:
#ffffff
;
border
:
1px
solid
#dee2e6
;
white-space
:
pre-wrap
;
}
.assistant-message
{
background-color
:
#f8f9fa
;
border
:
1px
solid
#dee2e6
;
}
.assistant-message
h1
,
.assistant-message
h2
,
.assistant-message
h3
{
margin-top
:
1rem
;
margin-bottom
:
.6rem
;
}
.assistant-message
p
{
margin
:
.5rem
0
;
}
.assistant-message
code
{
background
:
#fff
;
border
:
1px
solid
#e9ecef
;
border-radius
:
.25rem
;
padding
:
.05rem
.35rem
;
}
.assistant-message
pre
code
{
display
:
block
;
padding
:
.75rem
;
}
.assistant-message
ul
,
.assistant-message
ol
{
padding-left
:
1.25rem
;
}
.assistant-message
table
{
width
:
100%
;
margin
:
1rem
0
;
}
max-width
:
900px
;
margin
:
0
auto
1rem
auto
;
padding
:
1rem
1.25rem
;
border-radius
:
.75rem
;
line-height
:
1.6
;
font-size
:
1rem
;
word-break
:
break-word
;
}
.user-message
{
background-color
:
#ffffff
;
border
:
1px
solid
#dee2e6
;
white-space
:
pre-wrap
;
}
.assistant-message
{
background-color
:
#f8f9fa
;
border
:
1px
solid
#dee2e6
;
}
.assistant-message
h1
,
.assistant-message
h2
,
.assistant-message
h3
{
margin-top
:
1rem
;
margin-bottom
:
.6rem
;
}
.assistant-message
p
{
margin
:
.5rem
0
;
}
.assistant-message
code
{
background
:
#fff
;
border
:
1px
solid
#e9ecef
;
border-radius
:
.25rem
;
padding
:
.05rem
.35rem
;
}
.assistant-message
pre
code
{
display
:
block
;
padding
:
.75rem
;
}
.assistant-message
ul
,
.assistant-message
ol
{
padding-left
:
1.25rem
;
}
.assistant-message
table
{
width
:
100%
;
margin
:
1rem
0
;
}
/* First-run logo layout */
#mainScroll
{
display
:
flex
;
flex-direction
:
column
;
position
:
relative
;
}
#mainScroll
{
display
:
flex
;
flex-direction
:
column
;
position
:
relative
;
}
#welcomeLogo
{
flex
:
1
1
auto
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
user-select
:
none
;
}
#welcomeLogo
img
{
max-width
:
520px
;
width
:
100%
;
height
:
auto
;
filter
:
drop-shadow
(
0
6px
16px
rgba
(
0
,
0
,
0
,
.15
));
opacity
:
.98
;
}
#welcomeLogo
{
flex
:
1
1
auto
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
user-select
:
none
;
}
#welcomeLogo
img
{
max-width
:
520px
;
width
:
100%
;
height
:
auto
;
filter
:
drop-shadow
(
0
6px
16px
rgba
(
0
,
0
,
0
,
.15
));
opacity
:
.98
;
}
/* (kept, but the button is unused by logic now) */
#scrollDownBtn
{
display
:
none
;
}
</style>
</head>
<body>
<!-- Header -->
<header
class=
"py-3 bg-dark text-white shadow-sm"
>
...
...
@@ -175,13 +85,19 @@
<img
id=
"welcomeImg"
src=
"/static/images/ask-a-cat-en.png"
alt=
"Ask a Cat"
>
</div>
<div
id=
"thread"
></div>
<!-- Sentinel used to detect if user is at the bottom -->
<div
id=
"bottomSentinel"
style=
"height:1px;"
></div>
<!-- (Unused button) -->
<button
id=
"scrollDownBtn"
type=
"button"
aria-label=
"Scroll down"
></button>
</main>
<!-- Composer (sticks above fixed footer) -->
<div
class=
"composer py-3"
>
<div
class=
"container"
>
<form
id=
"promptForm"
class=
"d-flex flex-column gap-2"
>
<textarea
id=
"inputText"
class=
"form-control"
rows=
"
3
"
placeholder=
""
></textarea>
<textarea
id=
"inputText"
class=
"form-control"
rows=
"
1
"
placeholder=
""
></textarea>
<div
class=
"d-flex align-items-center gap-2"
>
<button
id=
"goBtn"
class=
"btn btn-primary"
type=
"submit"
></button>
<button
id=
"stopBtn"
class=
"btn btn-outline-danger"
type=
"button"
style=
"display:none"
></button>
...
...
@@ -196,7 +112,7 @@
<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>
// --- i18n setup ---
// --- i18n setup
(unchanged)
---
const
DEFAULT_LOCALE
=
'en'
;
const
SUPPORTED
=
[
'en'
,
'ru'
];
let
I18N
=
{};
...
...
@@ -243,6 +159,8 @@
ru
:
'/static/images/ask-a-cat-ru.png'
};
welcomeImg
.
src
=
srcByLocale
[
CURRENT_LOCALE
]
||
srcByLocale
.
en
;
// If we’re sticking to bottom, ensure any img load doesn’t break it
welcomeImg
.
addEventListener
(
'load'
,
()
=>
{
if
(
autoStick
)
scrollToBottom
();
},
{
once
:
true
});
}
function
applyI18n
()
{
document
.
title
=
t
(
'app.title'
,
'KotGPT'
);
...
...
@@ -263,6 +181,7 @@
const
disclaimer
=
document
.
getElementById
(
'disclaimer'
);
if
(
disclaimer
)
disclaimer
.
textContent
=
t
(
'footer.disclaimer'
,
'Beware: Cat can make mistakes.'
);
setWelcomeImage
();
if
(
autoStick
)
scrollToBottom
();
}
// --- UI refs ---
...
...
@@ -276,13 +195,47 @@
const
stopBtn
=
document
.
getElementById
(
'stopBtn'
);
const
welcomeLogo
=
document
.
getElementById
(
'welcomeLogo'
);
const
mainScroll
=
document
.
getElementById
(
'mainScroll'
);
const
bottomSentinel
=
document
.
getElementById
(
'bottomSentinel'
);
let
firstRequestDone
=
false
;
let
controller
=
null
;
// --- Stick-to-bottom logic ---
let
autoStick
=
true
;
// follow the bottom unless user scrolls up
function
scrollToBottom
(
smooth
=
false
)
{
if
(
!
mainScroll
)
return
;
// Use instant scroll during streaming to avoid stutter; smooth for jumps like new send
if
(
smooth
)
{
mainScroll
.
scrollTo
({
top
:
mainScroll
.
scrollHeight
,
behavior
:
'smooth'
});
}
else
{
mainScroll
.
scrollTop
=
mainScroll
.
scrollHeight
;
}
}
// Observe if sentinel is visible to determine if we’re at bottom
const
observer
=
new
IntersectionObserver
((
entries
)
=>
{
for
(
const
entry
of
entries
)
{
// If sentinel visible, we are at bottom -> enable autoStick
if
(
entry
.
target
===
bottomSentinel
)
{
autoStick
=
entry
.
isIntersecting
;
}
}
},
{
root
:
mainScroll
,
threshold
:
1.0
});
observer
.
observe
(
bottomSentinel
);
// When user scrolls up manually, autoStick will be turned off by observer.
// When they scroll back down (sentinel visible), it turns back on automatically.
// --- helpers ---
function
renderMarkdown
(
targetEl
,
markdownText
)
{
const
html
=
marked
.
parse
(
markdownText
,
{
breaks
:
true
,
gfm
:
true
,
headerIds
:
false
});
targetEl
.
innerHTML
=
DOMPurify
.
sanitize
(
html
,
{
USE_PROFILES
:
{
html
:
true
}
});
// If the assistant content produced
<
img
>
,
make
sure
their
eventual
load
keeps
us
at
bottom
.
const
imgs
=
targetEl
.
querySelectorAll
(
'img'
);
imgs
.
forEach
(
img
=>
{
img
.
addEventListener
(
'load'
,
()
=>
{
if
(
autoStick
)
scrollToBottom
();
});
});
}
function
showThinkingCat
(
targetEl
)
{
targetEl
.
innerHTML
=
''
;
...
...
@@ -291,17 +244,14 @@
img
.
src
=
CAT_GIF
+
'?t='
+
Date
.
now
();
img
.
alt
=
t
(
'status.thinking'
,
'Thinking…'
);
targetEl
.
appendChild
(
img
);
if
(
autoStick
)
scrollToBottom
();
}
function
clearThinkingCatIfPresent
(
targetEl
)
{
const
img
=
targetEl
.
querySelector
(
'img.thinking-cat'
);
if
(
img
)
targetEl
.
innerHTML
=
''
;
}
function
scrollToBottom
(
smooth
=
false
)
{
if
(
!
mainScroll
)
return
;
const
top
=
mainScroll
.
scrollHeight
-
mainScroll
.
clientHeight
;
mainScroll
.
scrollTo
({
top
,
behavior
:
smooth
?
'smooth'
:
'auto'
});
}
// --- Composer lock ---
function
setComposerLocked
(
locked
)
{
isRunning
=
locked
;
inputText
.
placeholder
=
locked
?
t
(
'status.thinking'
,
'Thinking…'
)
:
t
(
'composer.placeholder'
,
'What can I help you with?'
);
...
...
@@ -311,6 +261,7 @@
if
(
locked
)
inputText
.
blur
();
else
inputText
.
focus
();
}
// --- Flow ---
async
function
start
()
{
const
prompt
=
inputText
.
value
.
trim
();
if
(
!
prompt
)
return
;
...
...
@@ -321,13 +272,14 @@
userMsg
.
className
=
'message user-message'
;
userMsg
.
textContent
=
prompt
;
thread
.
appendChild
(
userMsg
);
scrollToBottom
();
const
assistantMsg
=
document
.
createElement
(
'div'
);
assistantMsg
.
className
=
'message assistant-message'
;
showThinkingCat
(
assistantMsg
);
thread
.
appendChild
(
assistantMsg
);
scrollToBottom
();
// New message -> we want a smooth jump to the end
if
(
autoStick
)
scrollToBottom
(
true
);
inputText
.
value
=
''
;
controller
=
new
AbortController
();
...
...
@@ -340,7 +292,7 @@
body
:
JSON
.
stringify
({
content
:
prompt
}),
signal
:
controller
.
signal
});
if
(
!
res
.
ok
||
!
res
.
body
)
throw
new
Error
(
'HTTP '
+
res
.
status
);
if
(
!
res
.
ok
||
!
res
.
body
)
throw
new
Error
(
`HTTP
${
res
.
status
}
`
);
const
reader
=
res
.
body
.
getReader
();
const
decoder
=
new
TextDecoder
();
...
...
@@ -355,15 +307,16 @@
rafPending
=
false
;
if
(
!
receivedAny
)
{
clearThinkingCatIfPresent
(
assistantMsg
);
receivedAny
=
true
;
}
renderMarkdown
(
assistantMsg
,
rawMarkdown
);
scrollToBottom
();
if
(
autoStick
)
scrollToBottom
();
// keep stuck during streaming
});
};
for
(;
;)
{
for
(;;)
{
const
{
value
,
done
}
=
await
reader
.
read
();
if
(
done
)
break
;
const
chunk
=
decoder
.
decode
(
value
,
{
stream
:
true
});
for
(
const
ch
of
chunk
)
rawMarkdown
+=
ch
;
// char-by-char
// char-by-char accumulation to mirror your behavior
for
(
const
ch
of
chunk
)
rawMarkdown
+=
ch
;
flush
();
}
flush
();
...
...
@@ -380,7 +333,7 @@
}
finally
{
setComposerLocked
(
false
);
controller
=
null
;
scrollToBottom
();
if
(
autoStick
)
scrollToBottom
();
}
}
...
...
@@ -390,11 +343,11 @@
}
// Events
document
.
getElementById
(
'promptForm'
)
.
addEventListener
(
'submit'
,
e
=>
{
promptForm
.
addEventListener
(
'submit'
,
e
=>
{
e
.
preventDefault
();
if
(
goBtn
.
style
.
display
!==
'none'
)
start
();
});
document
.
getElementById
(
'inputText'
)
.
addEventListener
(
'keydown'
,
e
=>
{
inputText
.
addEventListener
(
'keydown'
,
e
=>
{
if
(
e
.
key
===
'Enter'
&&
!
e
.
shiftKey
&&
goBtn
.
style
.
display
!==
'none'
)
{
e
.
preventDefault
();
start
();
...
...
@@ -402,14 +355,17 @@
});
document
.
getElementById
(
'stopBtn'
).
addEventListener
(
'click'
,
stop
);
// Keep to bottom on container resizes (e.g., window size changes)
window
.
addEventListener
(
'resize'
,
()
=>
{
if
(
autoStick
)
scrollToBottom
();
});
// Init
window
.
addEventListener
(
'DOMContentLoaded'
,
async
()
=>
{
try
{
await
fetch
(
'/api/clear'
,
{
method
:
'GET'
});
}
catch
(
_
)
{
}
await
loadLocale
(
CURRENT_LOCALE
);
document
.
getElementById
(
'inputText'
).
focus
();
scrollToBottom
();
inputText
.
focus
();
// First paint, ensure bottom (welcome image can be tall)
if
(
autoStick
)
scrollToBottom
();
});
</script>
</body>
</html>
\ No newline at end of file
</html>
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