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
7bd1e40b
Commit
7bd1e40b
authored
Aug 17, 2025
by
Michael Pastushkov
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix clear
parent
efce9518
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
312 additions
and
5 deletions
+312
-5
src/api/KotGPT.js
+12
-5
src/api/routes.js
+1
-0
static/www/index.html
+5
-0
static/www/index5.html
+294
-0
No files found.
src/api/KotGPT.js
View file @
7bd1e40b
...
@@ -10,7 +10,6 @@ const ConversationManager = require('./ConversationManager');
...
@@ -10,7 +10,6 @@ const ConversationManager = require('./ConversationManager');
const
MODEL
=
'gpt-oss:20b'
;
// 'llama3:8b'; // 'gpt-oss:20b';
const
MODEL
=
'gpt-oss:20b'
;
// 'llama3:8b'; // 'gpt-oss:20b';
const
EMBED_MODEL
=
'nomic-embed-text'
;
const
EMBED_MODEL
=
'nomic-embed-text'
;
const
LABEL_MODEL
=
'llama3:8b'
;
const
LABEL_MODEL
=
'llama3:8b'
;
const
COOKIE_TIMEOUT
=
1
*
1
*
10
*
60
*
1000
// 10 minutes
const
sessions
=
new
Map
();
// cookie -> ConversationManager
const
sessions
=
new
Map
();
// cookie -> ConversationManager
//const queues = new Map(); // cookie -> Promise (to serialize)
//const queues = new Map(); // cookie -> Promise (to serialize)
...
@@ -21,12 +20,11 @@ async function run(req, res) {
...
@@ -21,12 +20,11 @@ async function run(req, res) {
let
cookie
=
req
.
cookies
?.
session
;
let
cookie
=
req
.
cookies
?.
session
;
if
(
!
cookie
)
{
if
(
!
cookie
)
{
cookie
=
uuidv4
();
cookie
=
uuidv4
();
res
.
cookie
(
'session'
,
cookie
,
{
maxAge
:
COOKIE_TIMEOUT
});
res
.
cookie
(
'session'
,
cookie
,
{
path
:
'/'
,
sameSite
:
'Lax'
,
httpOnly
:
true
});
utils
.
log
(
`[session] New cookie set:
${
cookie
}
`
,
5
);
utils
.
log
(
`[session] New cookie set:
${
cookie
}
`
,
5
);
}
else
{
}
else
{
utils
.
log
(
`[session] Existing cookie:
${
cookie
}
`
,
5
);
utils
.
log
(
`[session] Existing cookie:
${
cookie
}
`
,
5
);
}
}
// Get conversation manager
// Get conversation manager
let
convman
=
sessions
.
get
(
cookie
);
let
convman
=
sessions
.
get
(
cookie
);
if
(
!
convman
)
{
if
(
!
convman
)
{
...
@@ -46,8 +44,16 @@ async function run(req, res) {
...
@@ -46,8 +44,16 @@ async function run(req, res) {
async
function
stop
(
req
,
res
)
{
async
function
stop
(
req
,
res
)
{
utils
.
log
(
'stop'
);
utils
.
log
(
'stop'
);
res
.
send
(
'Stop OK'
);
}
async
function
clear
(
req
,
res
)
{
res
.
clearCookie
(
'session'
,
{
path
:
'/'
,
sameSite
:
'Lax'
,
httpOnly
:
true
});
utils
.
log
(
'clear'
);
res
.
send
(
'Clear OK'
);
}
}
module
.
exports
=
{
run
,
stop
};
module
.
exports
=
{
run
,
stop
};
/* Testing * /
/* Testing * /
...
@@ -63,4 +69,6 @@ function log(data) {
...
@@ -63,4 +69,6 @@ function log(data) {
/**/
/**/
module
.
exports
.
run
=
run
;
module
.
exports
.
run
=
run
;
\ No newline at end of file
module
.
exports
.
stop
=
stop
;
module
.
exports
.
clear
=
clear
;
src/api/routes.js
View file @
7bd1e40b
...
@@ -21,6 +21,7 @@ function init(app) {
...
@@ -21,6 +21,7 @@ function init(app) {
// Misc
// Misc
[
POST
,
'/stream'
,
'./KotGPT'
],
[
POST
,
'/stream'
,
'./KotGPT'
],
[
GET
,
'/stop'
,
'./KotGPT'
,
'stop'
],
[
GET
,
'/stop'
,
'./KotGPT'
,
'stop'
],
[
GET
,
'/clear'
,
'./KotGPT'
,
'clear'
],
// Misc
// Misc
[
GET
,
'/info'
,
'../common/info'
],
[
GET
,
'/info'
,
'../common/info'
],
[
GET
,
'/logcat'
,
'../utils/logcat'
],
[
GET
,
'/logcat'
,
'../utils/logcat'
],
...
...
static/www/index.html
View file @
7bd1e40b
...
@@ -178,6 +178,11 @@
...
@@ -178,6 +178,11 @@
let
controller
=
null
;
let
controller
=
null
;
window
.
addEventListener
(
'DOMContentLoaded'
,
async
()
=>
{
window
.
addEventListener
(
'DOMContentLoaded'
,
async
()
=>
{
try
{
await
fetch
(
'/api/clear'
,
{
method
:
'GET'
});
}
catch
(
err
)
{
// nothing to do
}
await
loadLocale
(
CURRENT_LOCALE
);
await
loadLocale
(
CURRENT_LOCALE
);
inputText
.
focus
();
inputText
.
focus
();
});
});
...
...
static/www/index5.html
0 → 100755
View file @
7bd1e40b
<!doctype html>
<html
lang=
"en"
>
<head>
<meta
charset=
"utf-8"
/>
<title>
KotGPT
</title>
<meta
name=
"viewport"
content=
"width=device-width, initial-scale=1"
/>
<link
href=
"https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
rel=
"stylesheet"
/>
<style>
body
{
min-height
:
100vh
;
display
:
flex
;
flex-direction
:
column
;
background-color
:
#f8f9fa
;
}
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
;
}
.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
;
}
#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
;
}
</style>
</head>
<body>
<!-- Header -->
<header
class=
"py-3 bg-dark text-white shadow-sm"
>
<div
class=
"container d-flex justify-content-between align-items-center"
>
<div
class=
"d-flex align-items-center gap-2"
>
<img
id=
"catIcon"
class=
"cat-spinner"
src=
"/static/images/cat-still.png"
alt=
"Cat logo"
/>
<h1
class=
"h4 mb-0"
>
KotGPT
</h1>
</div>
</div>
</header>
<!-- Conversation -->
<main
class=
"container my-3"
id=
"mainScroll"
>
<div
id=
"welcomeLogo"
>
<img
id=
"welcomeImg"
src=
"/static/images/ask-a-cat-en.png"
alt=
"Ask a Cat"
>
</div>
<div
id=
"thread"
></div>
</main>
<!-- Composer -->
<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>
<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>
</div>
</form>
</div>
</div>
<!-- Disclaimer Footer -->
<footer
id=
"disclaimer"
>
<!-- 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/dompurify@3.1.6/dist/purify.min.js"
></script>
<script>
// --- i18n setup ---
const
DEFAULT_LOCALE
=
'en'
;
const
SUPPORTED
=
[
'en'
,
'ru'
];
let
I18N
=
{};
let
CURRENT_LOCALE
=
detectLocale
();
let
isRunning
=
false
;
function
getLangFromQuery
()
{
const
m
=
location
.
search
.
match
(
/
[
?&
]
lang=
(
en|ru
)\b
/i
);
return
m
?
m
[
1
].
toLowerCase
()
:
null
;
}
function
detectLocale
()
{
const
q
=
getLangFromQuery
();
if
(
q
&&
SUPPORTED
.
includes
(
q
))
return
q
;
const
nav
=
(
navigator
.
language
||
''
).
toLowerCase
();
if
(
nav
.
startsWith
(
'ru'
))
return
'ru'
;
return
DEFAULT_LOCALE
;
}
async
function
loadLocale
(
locale
)
{
try
{
const
res
=
await
fetch
(
`/locales/
${
locale
}
.json`
,
{
cache
:
'no-store'
});
if
(
!
res
.
ok
)
throw
new
Error
(
'HTTP '
+
res
.
status
);
I18N
=
await
res
.
json
();
CURRENT_LOCALE
=
locale
;
document
.
documentElement
.
lang
=
locale
;
applyI18n
();
}
catch
(
e
)
{
if
(
locale
!==
DEFAULT_LOCALE
)
await
loadLocale
(
DEFAULT_LOCALE
);
}
}
function
t
(
key
,
fallback
=
''
)
{
const
parts
=
key
.
split
(
'.'
);
let
cur
=
I18N
;
for
(
const
p
of
parts
)
{
if
(
cur
&&
Object
.
prototype
.
hasOwnProperty
.
call
(
cur
,
p
))
cur
=
cur
[
p
];
else
return
fallback
||
key
;
}
return
(
typeof
cur
===
'string'
)
?
cur
:
(
fallback
||
key
);
}
function
setWelcomeImage
()
{
const
welcomeImg
=
document
.
getElementById
(
'welcomeImg'
);
if
(
!
welcomeImg
)
return
;
const
srcByLocale
=
{
en
:
'/static/images/ask-a-cat-en.png'
,
ru
:
'/static/images/ask-a-cat-ru.png'
};
welcomeImg
.
src
=
srcByLocale
[
CURRENT_LOCALE
]
||
srcByLocale
.
en
;
}
function
applyI18n
()
{
document
.
title
=
t
(
'app.title'
,
'KotGPT'
);
const
catIcon
=
document
.
getElementById
(
'catIcon'
);
if
(
catIcon
)
catIcon
.
alt
=
t
(
'header.logoAlt'
,
'Cat logo'
);
const
welcomeImg
=
document
.
getElementById
(
'welcomeImg'
);
if
(
welcomeImg
)
welcomeImg
.
alt
=
t
(
'main.welcomeAlt'
,
'Ask a Cat'
);
const
inputText
=
document
.
getElementById
(
'inputText'
);
if
(
inputText
)
{
inputText
.
placeholder
=
isRunning
?
t
(
'status.thinking'
,
'Thinking…'
)
:
t
(
'composer.placeholder'
,
'What can I help you with?'
);
}
const
goBtn
=
document
.
getElementById
(
'goBtn'
);
if
(
goBtn
)
goBtn
.
textContent
=
t
(
'buttons.go'
,
'Go'
);
const
stopBtn
=
document
.
getElementById
(
'stopBtn'
);
if
(
stopBtn
)
stopBtn
.
textContent
=
t
(
'buttons.stop'
,
'Stop'
);
const
disclaimer
=
document
.
getElementById
(
'disclaimer'
);
if
(
disclaimer
)
disclaimer
.
textContent
=
t
(
'footer.disclaimer'
,
'Beware: Cat can make mistakes.'
);
setWelcomeImage
();
}
// --- UI elements ---
const
ENDPOINT
=
'/api/stream'
;
const
STOPPOINT
=
'/api/stop'
;
const
CAT_GIF
=
'/static/images/cat-progress.gif'
;
const
thread
=
document
.
getElementById
(
'thread'
);
const
promptForm
=
document
.
getElementById
(
'promptForm'
);
const
inputText
=
document
.
getElementById
(
'inputText'
);
const
goBtn
=
document
.
getElementById
(
'goBtn'
);
const
stopBtn
=
document
.
getElementById
(
'stopBtn'
);
const
welcomeLogo
=
document
.
getElementById
(
'welcomeLogo'
);
let
firstRequestDone
=
false
;
let
controller
=
null
;
window
.
addEventListener
(
'DOMContentLoaded'
,
async
()
=>
{
await
loadLocale
(
CURRENT_LOCALE
);
inputText
.
focus
();
});
function
renderMarkdown
(
targetEl
,
markdownText
)
{
const
html
=
marked
.
parse
(
markdownText
,
{
breaks
:
true
,
gfm
:
true
,
headerIds
:
false
});
targetEl
.
innerHTML
=
DOMPurify
.
sanitize
(
html
,
{
USE_PROFILES
:{
html
:
true
}
});
}
function
showThinkingCat
(
targetEl
)
{
targetEl
.
innerHTML
=
''
;
const
img
=
document
.
createElement
(
'img'
);
img
.
className
=
'thinking-cat'
;
img
.
src
=
CAT_GIF
+
'?t='
+
Date
.
now
();
img
.
alt
=
t
(
'status.thinking'
,
'Thinking…'
);
targetEl
.
appendChild
(
img
);
}
function
clearThinkingCatIfPresent
(
targetEl
)
{
const
img
=
targetEl
.
querySelector
(
'img.thinking-cat'
);
if
(
img
)
targetEl
.
innerHTML
=
''
;
}
function
setComposerLocked
(
locked
)
{
isRunning
=
locked
;
inputText
.
placeholder
=
locked
?
t
(
'status.thinking'
,
'Thinking…'
)
:
t
(
'composer.placeholder'
,
'What can I help you with?'
);
inputText
.
placeholder
=
inputText
.
placeholder
;
void
inputText
.
offsetHeight
;
inputText
.
disabled
=
locked
;
goBtn
.
style
.
display
=
locked
?
'none'
:
'inline-block'
;
stopBtn
.
style
.
display
=
locked
?
'inline-block'
:
'none'
;
if
(
locked
)
inputText
.
blur
();
else
inputText
.
focus
();
}
async
function
start
()
{
const
prompt
=
inputText
.
value
.
trim
();
if
(
!
prompt
)
return
;
if
(
!
firstRequestDone
)
{
welcomeLogo
?.
remove
();
firstRequestDone
=
true
;
}
const
userMsg
=
document
.
createElement
(
'div'
);
userMsg
.
className
=
'message user-message'
;
userMsg
.
textContent
=
prompt
;
thread
.
appendChild
(
userMsg
);
const
assistantMsg
=
document
.
createElement
(
'div'
);
assistantMsg
.
className
=
'message assistant-message'
;
showThinkingCat
(
assistantMsg
);
thread
.
appendChild
(
assistantMsg
);
inputText
.
value
=
''
;
controller
=
new
AbortController
();
setComposerLocked
(
true
);
try
{
const
res
=
await
fetch
(
ENDPOINT
,
{
method
:
'POST'
,
headers
:
{
'Content-Type'
:
'application/json'
},
body
:
JSON
.
stringify
({
content
:
prompt
}),
signal
:
controller
.
signal
});
if
(
!
res
.
ok
||
!
res
.
body
)
throw
new
Error
(
`HTTP
${
res
.
status
}
`
);
const
reader
=
res
.
body
.
getReader
();
const
decoder
=
new
TextDecoder
();
let
rawMarkdown
=
''
;
let
receivedAny
=
false
;
let
rafPending
=
false
;
const
flush
=
()
=>
{
if
(
rafPending
)
return
;
rafPending
=
true
;
requestAnimationFrame
(()
=>
{
rafPending
=
false
;
if
(
!
receivedAny
)
{
clearThinkingCatIfPresent
(
assistantMsg
);
receivedAny
=
true
;
}
renderMarkdown
(
assistantMsg
,
rawMarkdown
);
});
};
for
(;;)
{
const
{
value
,
done
}
=
await
reader
.
read
();
if
(
done
)
break
;
const
chunk
=
decoder
.
decode
(
value
,
{
stream
:
true
});
for
(
const
ch
of
chunk
)
{
rawMarkdown
+=
ch
;
flush
();
}
}
flush
();
}
catch
(
err
)
{
if
(
err
.
name
!==
'AbortError'
)
{
clearThinkingCatIfPresent
(
assistantMsg
);
renderMarkdown
(
assistantMsg
,
`\n\n**
${
t
(
'status.errorLabel'
,
'Error'
)}
:**
${
err
.
message
||
err
}
`
);
}
else
{
if
(
assistantMsg
.
querySelector
(
'img.thinking-cat'
))
{
clearThinkingCatIfPresent
(
assistantMsg
);
renderMarkdown
(
assistantMsg
,
t
(
'status.stopped'
,
'_Stopped._'
));
}
}
}
finally
{
setComposerLocked
(
false
);
controller
=
null
;
}
}
async
function
stop
()
{
if
(
controller
)
controller
.
abort
();
try
{
await
fetch
(
STOPPOINT
,
{
method
:
'POST'
});
}
catch
(
_
)
{}
}
promptForm
.
addEventListener
(
'submit'
,
e
=>
{
e
.
preventDefault
();
if
(
goBtn
.
style
.
display
!==
'none'
)
start
();
});
inputText
.
addEventListener
(
'keydown'
,
e
=>
{
if
(
e
.
key
===
'Enter'
&&
!
e
.
shiftKey
&&
goBtn
.
style
.
display
!==
'none'
)
{
e
.
preventDefault
();
start
();
}
});
stopBtn
.
addEventListener
(
'click'
,
stop
);
</script>
</body>
</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