Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
S
Son Of Anton
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
Administrator
Son Of Anton
Commits
015b7902
Commit
015b7902
authored
Apr 06, 2026
by
Mahmoud Aglan
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
bg
parent
fcd5022b
Changes
2
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
119 additions
and
562 deletions
+119
-562
utility_routes.py
backend/routes/utility_routes.py
+0
-515
api.js
frontend/src/api.js
+119
-47
No files found.
backend/routes/utility_routes.py
deleted
100644 → 0
View file @
fcd5022b
"""
Utility endpoints — chat search, export, regenerate, usage analytics.
Son of Anton v4.2.0 — NEW TOOLS
"""
import
json
import
re
from
datetime
import
datetime
,
timedelta
from
typing
import
Optional
from
pydantic
import
BaseModel
from
fastapi
import
APIRouter
,
Depends
,
HTTPException
,
Query
from
fastapi.responses
import
StreamingResponse
from
sqlalchemy.orm
import
Session
from
sqlalchemy
import
func
,
or_
from
backend.database
import
get_db
from
backend.models
import
User
,
Chat
,
Message
,
ChatAttachment
from
backend.auth
import
get_current_user
router
=
APIRouter
()
# ═══════════════════════════════════════════════════
# CHAT SEARCH — find messages across all your chats
# ═══════════════════════════════════════════════════
@
router
.
get
(
"/search"
)
def
search_messages
(
q
:
str
=
Query
(
...
,
min_length
=
2
,
max_length
=
500
),
limit
:
int
=
Query
(
50
,
ge
=
1
,
le
=
200
),
user
:
User
=
Depends
(
get_current_user
),
db
:
Session
=
Depends
(
get_db
),
):
"""Search through all messages in user's chats."""
search_term
=
f
"
%
{q}
%
"
results
=
(
db
.
query
(
Message
,
Chat
.
title
,
Chat
.
id
)
.
join
(
Chat
,
Message
.
chat_id
==
Chat
.
id
)
.
filter
(
Chat
.
user_id
==
user
.
id
,
Message
.
content
.
ilike
(
search_term
),
)
.
order_by
(
Message
.
created_at
.
desc
())
.
limit
(
limit
)
.
all
()
)
return
{
"query"
:
q
,
"count"
:
len
(
results
),
"results"
:
[
{
"message_id"
:
msg
.
id
,
"chat_id"
:
chat_id
,
"chat_title"
:
chat_title
,
"role"
:
msg
.
role
,
"content_preview"
:
_highlight_match
(
msg
.
content
or
""
,
q
,
300
),
"created_at"
:
str
(
msg
.
created_at
),
}
for
msg
,
chat_title
,
chat_id
in
results
],
}
def
_highlight_match
(
text
:
str
,
query
:
str
,
max_len
:
int
=
300
)
->
str
:
"""Extract a snippet around the match."""
lower
=
text
.
lower
()
idx
=
lower
.
find
(
query
.
lower
())
if
idx
==
-
1
:
return
text
[:
max_len
]
start
=
max
(
0
,
idx
-
80
)
end
=
min
(
len
(
text
),
idx
+
len
(
query
)
+
220
)
snippet
=
text
[
start
:
end
]
if
start
>
0
:
snippet
=
"…"
+
snippet
if
end
<
len
(
text
):
snippet
=
snippet
+
"…"
return
snippet
# ═══════════════════════════════════════════════════
# CHAT EXPORT — download full chat as markdown
# ═══════════════════════════════════════════════════
@
router
.
get
(
"/chats/{chat_id}/export"
)
def
export_chat_markdown
(
chat_id
:
str
,
user
:
User
=
Depends
(
get_current_user
),
db
:
Session
=
Depends
(
get_db
),
):
"""Export entire chat as a clean markdown file."""
chat
=
db
.
query
(
Chat
)
.
filter
(
Chat
.
id
==
chat_id
,
Chat
.
user_id
==
user
.
id
)
.
first
()
if
not
chat
:
raise
HTTPException
(
404
,
"Chat not found"
)
messages
=
(
db
.
query
(
Message
)
.
filter
(
Message
.
chat_id
==
chat_id
)
.
order_by
(
Message
.
created_at
)
.
all
()
)
lines
=
[
f
"# {chat.title}"
,
f
""
,
f
"**Model:** {chat.model}"
,
f
"**Created:** {chat.created_at}"
,
f
"**Messages:** {len(messages)}"
,
f
""
,
f
"---"
,
f
""
,
]
total_input
=
0
total_output
=
0
for
msg
in
messages
:
role_label
=
"🧑 **User**"
if
msg
.
role
==
"user"
else
"🔥 **Son of Anton**"
lines
.
append
(
f
"### {role_label}"
)
lines
.
append
(
f
"*{msg.created_at}*"
)
lines
.
append
(
""
)
if
msg
.
thinking_content
:
lines
.
append
(
"<details>"
)
lines
.
append
(
"<summary>🧠 Reasoning</summary>"
)
lines
.
append
(
""
)
lines
.
append
(
msg
.
thinking_content
)
lines
.
append
(
""
)
lines
.
append
(
"</details>"
)
lines
.
append
(
""
)
lines
.
append
(
msg
.
content
or
"*(empty)*"
)
lines
.
append
(
""
)
if
msg
.
input_tokens
or
msg
.
output_tokens
:
total_input
+=
msg
.
input_tokens
or
0
total_output
+=
msg
.
output_tokens
or
0
lines
.
append
(
"---"
)
lines
.
append
(
""
)
lines
.
append
(
f
"## Token Usage"
)
lines
.
append
(
f
"- Input: {total_input:,}"
)
lines
.
append
(
f
"- Output: {total_output:,}"
)
lines
.
append
(
f
"- Total: {total_input + total_output:,}"
)
lines
.
append
(
""
)
lines
.
append
(
"*Exported from Son of Anton*"
)
md
=
"
\n
"
.
join
(
lines
)
safe_title
=
re
.
sub
(
r'[^\w\s-]'
,
''
,
chat
.
title
)
.
strip
()
safe_title
=
re
.
sub
(
r'\s+'
,
'-'
,
safe_title
)[:
60
]
or
"chat-export"
import
io
return
StreamingResponse
(
io
.
BytesIO
(
md
.
encode
(
"utf-8"
)),
media_type
=
"text/markdown"
,
headers
=
{
"Content-Disposition"
:
f
'attachment; filename="{safe_title}.md"'
},
)
# ═══════════════════════════════════════════════════
# EXPORT CHAT AS JSON (machine-readable)
# ═══════════════════════════════════════════════════
@
router
.
get
(
"/chats/{chat_id}/export-json"
)
def
export_chat_json
(
chat_id
:
str
,
user
:
User
=
Depends
(
get_current_user
),
db
:
Session
=
Depends
(
get_db
),
):
"""Export chat as structured JSON."""
chat
=
db
.
query
(
Chat
)
.
filter
(
Chat
.
id
==
chat_id
,
Chat
.
user_id
==
user
.
id
)
.
first
()
if
not
chat
:
raise
HTTPException
(
404
,
"Chat not found"
)
messages
=
(
db
.
query
(
Message
)
.
filter
(
Message
.
chat_id
==
chat_id
)
.
order_by
(
Message
.
created_at
)
.
all
()
)
data
=
{
"chat"
:
{
"id"
:
chat
.
id
,
"title"
:
chat
.
title
,
"model"
:
chat
.
model
,
"created_at"
:
str
(
chat
.
created_at
),
"updated_at"
:
str
(
chat
.
updated_at
),
},
"messages"
:
[
{
"id"
:
m
.
id
,
"role"
:
m
.
role
,
"content"
:
m
.
content
,
"thinking_content"
:
m
.
thinking_content
,
"input_tokens"
:
m
.
input_tokens
,
"output_tokens"
:
m
.
output_tokens
,
"created_at"
:
str
(
m
.
created_at
),
}
for
m
in
messages
],
"exported_at"
:
datetime
.
utcnow
()
.
isoformat
(),
"exported_by"
:
user
.
username
,
}
safe_title
=
re
.
sub
(
r'[^\w\s-]'
,
''
,
chat
.
title
)
.
strip
()
safe_title
=
re
.
sub
(
r'\s+'
,
'-'
,
safe_title
)[:
60
]
or
"chat-export"
import
io
return
StreamingResponse
(
io
.
BytesIO
(
json
.
dumps
(
data
,
indent
=
2
,
ensure_ascii
=
False
)
.
encode
(
"utf-8"
)),
media_type
=
"application/json"
,
headers
=
{
"Content-Disposition"
:
f
'attachment; filename="{safe_title}.json"'
},
)
# ═══════════════════════════════════════════════════
# USER ANALYTICS — your personal usage dashboard
# ═══════════════════════════════════════════════════
@
router
.
get
(
"/my-stats"
)
def
my_stats
(
user
:
User
=
Depends
(
get_current_user
),
db
:
Session
=
Depends
(
get_db
),
):
"""Get personal usage statistics."""
now
=
datetime
.
utcnow
()
today_start
=
now
.
replace
(
hour
=
0
,
minute
=
0
,
second
=
0
,
microsecond
=
0
)
week_start
=
today_start
-
timedelta
(
days
=
7
)
month_start
=
today_start
.
replace
(
day
=
1
)
total_chats
=
db
.
query
(
Chat
)
.
filter
(
Chat
.
user_id
==
user
.
id
)
.
count
()
total_messages
=
(
db
.
query
(
Message
)
.
join
(
Chat
)
.
filter
(
Chat
.
user_id
==
user
.
id
)
.
count
()
)
messages_today
=
(
db
.
query
(
Message
)
.
join
(
Chat
)
.
filter
(
Chat
.
user_id
==
user
.
id
,
Message
.
role
==
"user"
,
Message
.
created_at
>=
today_start
)
.
count
()
)
messages_this_week
=
(
db
.
query
(
Message
)
.
join
(
Chat
)
.
filter
(
Chat
.
user_id
==
user
.
id
,
Message
.
role
==
"user"
,
Message
.
created_at
>=
week_start
)
.
count
()
)
# Token usage breakdown
token_data
=
(
db
.
query
(
func
.
sum
(
Message
.
input_tokens
),
func
.
sum
(
Message
.
output_tokens
),
)
.
join
(
Chat
)
.
filter
(
Chat
.
user_id
==
user
.
id
,
Message
.
role
==
"assistant"
)
.
first
()
)
total_input_tokens
=
token_data
[
0
]
or
0
total_output_tokens
=
token_data
[
1
]
or
0
# Most active chats
active_chats
=
(
db
.
query
(
Chat
.
id
,
Chat
.
title
,
func
.
count
(
Message
.
id
)
.
label
(
"msg_count"
))
.
join
(
Message
)
.
filter
(
Chat
.
user_id
==
user
.
id
)
.
group_by
(
Chat
.
id
,
Chat
.
title
)
.
order_by
(
func
.
count
(
Message
.
id
)
.
desc
())
.
limit
(
5
)
.
all
()
)
# Model usage breakdown
model_usage
=
(
db
.
query
(
Chat
.
model
,
func
.
count
(
Chat
.
id
))
.
filter
(
Chat
.
user_id
==
user
.
id
)
.
group_by
(
Chat
.
model
)
.
all
()
)
# Daily message counts for last 7 days
daily_counts
=
[]
for
i
in
range
(
7
):
day_start
=
today_start
-
timedelta
(
days
=
i
)
day_end
=
day_start
+
timedelta
(
days
=
1
)
count
=
(
db
.
query
(
Message
)
.
join
(
Chat
)
.
filter
(
Chat
.
user_id
==
user
.
id
,
Message
.
role
==
"user"
,
Message
.
created_at
>=
day_start
,
Message
.
created_at
<
day_end
,
)
.
count
()
)
daily_counts
.
append
({
"date"
:
day_start
.
strftime
(
"
%
Y-
%
m-
%
d"
),
"day"
:
day_start
.
strftime
(
"
%
a"
),
"messages"
:
count
,
})
daily_counts
.
reverse
()
# Attachment stats
att_count
=
(
db
.
query
(
ChatAttachment
)
.
join
(
Chat
,
ChatAttachment
.
chat_id
==
Chat
.
id
)
.
filter
(
Chat
.
user_id
==
user
.
id
)
.
count
()
)
att_size
=
(
db
.
query
(
func
.
sum
(
ChatAttachment
.
file_size
))
.
join
(
Chat
,
ChatAttachment
.
chat_id
==
Chat
.
id
)
.
filter
(
Chat
.
user_id
==
user
.
id
)
.
scalar
()
or
0
)
return
{
"user"
:
{
"username"
:
user
.
username
,
"role"
:
user
.
role
,
"quota_monthly"
:
user
.
quota_tokens_monthly
,
"tokens_used_this_month"
:
user
.
tokens_used_this_month
,
"quota_remaining"
:
max
(
0
,
user
.
quota_tokens_monthly
-
user
.
tokens_used_this_month
),
"quota_percent_used"
:
round
(
(
user
.
tokens_used_this_month
/
max
(
user
.
quota_tokens_monthly
,
1
))
*
100
,
1
),
},
"totals"
:
{
"chats"
:
total_chats
,
"messages"
:
total_messages
,
"input_tokens"
:
total_input_tokens
,
"output_tokens"
:
total_output_tokens
,
"total_tokens"
:
total_input_tokens
+
total_output_tokens
,
"attachments"
:
att_count
,
"attachment_size_mb"
:
round
(
att_size
/
1024
/
1024
,
2
),
},
"activity"
:
{
"messages_today"
:
messages_today
,
"messages_this_week"
:
messages_this_week
,
"daily_activity"
:
daily_counts
,
},
"top_chats"
:
[
{
"id"
:
cid
,
"title"
:
title
,
"message_count"
:
cnt
}
for
cid
,
title
,
cnt
in
active_chats
],
"model_usage"
:
{
model
:
count
for
model
,
count
in
model_usage
},
}
# ═══════════════════════════════════════════════════
# DELETE MESSAGE — remove a specific message
# ═══════════════════════════════════════════════════
@
router
.
delete
(
"/chats/{chat_id}/messages/{message_id}"
)
def
delete_message
(
chat_id
:
str
,
message_id
:
str
,
user
:
User
=
Depends
(
get_current_user
),
db
:
Session
=
Depends
(
get_db
),
):
"""Delete a specific message from a chat."""
chat
=
db
.
query
(
Chat
)
.
filter
(
Chat
.
id
==
chat_id
,
Chat
.
user_id
==
user
.
id
)
.
first
()
if
not
chat
:
raise
HTTPException
(
404
,
"Chat not found"
)
msg
=
db
.
query
(
Message
)
.
filter
(
Message
.
id
==
message_id
,
Message
.
chat_id
==
chat_id
)
.
first
()
if
not
msg
:
raise
HTTPException
(
404
,
"Message not found"
)
# Also unlink any attachments
db
.
query
(
ChatAttachment
)
.
filter
(
ChatAttachment
.
message_id
==
message_id
)
.
update
(
{
"message_id"
:
None
}
)
db
.
delete
(
msg
)
db
.
commit
()
return
{
"ok"
:
True
}
# ═══════════════════════════════════════════════════
# DUPLICATE CHAT — fork a conversation
# ═══════════════════════════════════════════════════
class
ForkChatBody
(
BaseModel
):
new_title
:
Optional
[
str
]
=
None
up_to_message_id
:
Optional
[
str
]
=
None
@
router
.
post
(
"/chats/{chat_id}/fork"
)
def
fork_chat
(
chat_id
:
str
,
body
:
ForkChatBody
=
ForkChatBody
(),
user
:
User
=
Depends
(
get_current_user
),
db
:
Session
=
Depends
(
get_db
),
):
"""Duplicate a chat, optionally up to a specific message."""
original
=
db
.
query
(
Chat
)
.
filter
(
Chat
.
id
==
chat_id
,
Chat
.
user_id
==
user
.
id
)
.
first
()
if
not
original
:
raise
HTTPException
(
404
,
"Chat not found"
)
new_title
=
body
.
new_title
or
f
"{original.title} (fork)"
new_chat
=
Chat
(
user_id
=
user
.
id
,
title
=
new_title
[:
200
],
model
=
original
.
model
,
knowledge_base_id
=
original
.
knowledge_base_id
,
linked_repo_id
=
original
.
linked_repo_id
,
max_tokens
=
original
.
max_tokens
,
reasoning_budget
=
original
.
reasoning_budget
,
)
db
.
add
(
new_chat
)
db
.
commit
()
db
.
refresh
(
new_chat
)
messages
=
(
db
.
query
(
Message
)
.
filter
(
Message
.
chat_id
==
chat_id
)
.
order_by
(
Message
.
created_at
)
.
all
()
)
copied
=
0
for
msg
in
messages
:
new_msg
=
Message
(
chat_id
=
new_chat
.
id
,
role
=
msg
.
role
,
content
=
msg
.
content
,
thinking_content
=
msg
.
thinking_content
,
input_tokens
=
msg
.
input_tokens
,
output_tokens
=
msg
.
output_tokens
,
)
db
.
add
(
new_msg
)
copied
+=
1
if
body
.
up_to_message_id
and
msg
.
id
==
body
.
up_to_message_id
:
break
db
.
commit
()
return
{
"ok"
:
True
,
"new_chat_id"
:
new_chat
.
id
,
"title"
:
new_chat
.
title
,
"messages_copied"
:
copied
,
}
# ═══════════════════════════════════════════════════
# BOOKMARK / PIN messages
# ═══════════════════════════════════════════════════
@
router
.
get
(
"/chats/{chat_id}/code-blocks"
)
def
extract_chat_code_blocks
(
chat_id
:
str
,
user
:
User
=
Depends
(
get_current_user
),
db
:
Session
=
Depends
(
get_db
),
):
"""Extract ALL code blocks from every assistant message in a chat."""
chat
=
db
.
query
(
Chat
)
.
filter
(
Chat
.
id
==
chat_id
,
Chat
.
user_id
==
user
.
id
)
.
first
()
if
not
chat
:
raise
HTTPException
(
404
,
"Chat not found"
)
from
backend.services.code_extractor
import
extract_code_blocks
messages
=
(
db
.
query
(
Message
)
.
filter
(
Message
.
chat_id
==
chat_id
,
Message
.
role
==
"assistant"
)
.
order_by
(
Message
.
created_at
)
.
all
()
)
all_blocks
=
[]
seen_files
=
{}
for
msg
in
messages
:
if
not
msg
.
content
:
continue
blocks
=
extract_code_blocks
(
msg
.
content
)
for
block
in
blocks
:
# Keep track of latest version of each file
seen_files
[
block
[
"filename"
]]
=
block
all_blocks
.
append
({
**
block
,
"message_id"
:
msg
.
id
,
"message_date"
:
str
(
msg
.
created_at
),
})
# Deduplicated = latest version of each filename
latest_files
=
list
(
seen_files
.
values
())
return
{
"chat_id"
:
chat_id
,
"chat_title"
:
chat
.
title
,
"total_blocks"
:
len
(
all_blocks
),
"unique_files"
:
len
(
latest_files
),
"all_blocks"
:
all_blocks
,
"latest_files"
:
latest_files
,
}
\ No newline at end of file
frontend/src/api.js
View file @
015b7902
// ═══════════════════════════════════════════════════
// UTILITY TOOLS — v4.2.0 enhancements
// ═══════════════════════════════════════════════════
export
const
searchMessages
=
(
token
,
query
,
limit
=
50
)
=>
request
(
"GET"
,
`/utils/search?q=
${
encodeURIComponent
(
query
)}
&limit=
${
limit
}
`
,
token
);
export
const
getMyStats
=
(
token
)
=>
request
(
"GET"
,
"/utils/my-stats"
,
token
);
export
const
forkChat
=
(
token
,
chatId
,
data
=
{})
=>
request
(
"POST"
,
`/utils/chats/
${
chatId
}
/fork`
,
token
,
data
);
export
const
deleteMessage
=
(
token
,
chatId
,
messageId
)
=>
request
(
"DELETE"
,
`/utils/chats/
${
chatId
}
/messages/
${
messageId
}
`
,
token
);
export
const
getChatCodeBlocks
=
(
token
,
chatId
)
=>
request
(
"GET"
,
`/utils/chats/
${
chatId
}
/code-blocks`
,
token
);
export
function
exportChatMarkdown
(
token
,
chatId
)
{
return
fetch
(
`
${
BASE
}
/utils/chats/
${
chatId
}
/export`
,
{
headers
:
{
Authorization
:
`Bearer
${
token
}
`
},
}).
then
((
res
)
=>
{
if
(
!
res
.
ok
)
throw
new
Error
(
"Export failed"
);
return
res
.
blob
();
}).
then
((
blob
)
=>
{
const
url
=
URL
.
createObjectURL
(
blob
);
const
a
=
document
.
createElement
(
"a"
);
a
.
href
=
url
;
a
.
download
=
"chat-export.md"
;
a
.
click
();
URL
.
revokeObjectURL
(
url
);
});
const
BASE
=
"/api"
;
function
headers
(
token
)
{
const
h
=
{
"Content-Type"
:
"application/json"
};
if
(
token
)
h
[
"Authorization"
]
=
`Bearer
${
token
}
`
;
return
h
;
}
function
authHeader
(
token
)
{
return
token
?
{
Authorization
:
`Bearer
${
token
}
`
}
:
{};
}
function
extractError
(
err
,
d
)
{
let
m
=
err
.
detail
||
err
.
message
||
d
;
if
(
Array
.
isArray
(
m
))
return
m
.
map
(
x
=>
x
.
msg
||
JSON
.
stringify
(
x
)).
join
(
", "
);
if
(
typeof
m
===
"object"
)
return
m
.
message
||
JSON
.
stringify
(
m
);
return
String
(
m
);
}
async
function
request
(
method
,
path
,
token
,
body
)
{
const
opts
=
{
method
,
headers
:
headers
(
token
)
};
if
(
body
)
opts
.
body
=
JSON
.
stringify
(
body
);
const
res
=
await
fetch
(
`
${
BASE
}${
path
}
`
,
opts
);
if
(
!
res
.
ok
)
{
const
err
=
await
res
.
json
().
catch
(()
=>
({
detail
:
res
.
statusText
}));
throw
new
Error
(
extractError
(
err
,
"Request failed"
));
}
return
res
.
json
();
}
export
function
exportChatJson
(
token
,
chatId
)
{
return
fetch
(
`
${
BASE
}
/utils/chats/
${
chatId
}
/export-json`
,
{
headers
:
{
Authorization
:
`Bearer
${
token
}
`
},
}).
then
((
res
)
=>
{
if
(
!
res
.
ok
)
throw
new
Error
(
"Export failed"
);
return
res
.
blob
();
}).
then
((
blob
)
=>
{
const
url
=
URL
.
createObjectURL
(
blob
);
const
a
=
document
.
createElement
(
"a"
);
a
.
href
=
url
;
a
.
download
=
"chat-export.json"
;
a
.
click
();
URL
.
revokeObjectURL
(
url
);
});
}
\ No newline at end of file
// Auth
export
const
login
=
(
u
,
p
)
=>
request
(
"POST"
,
"/auth/login"
,
null
,
{
username
:
u
,
password
:
p
});
export
const
register
=
(
u
,
e
,
p
)
=>
request
(
"POST"
,
"/auth/register"
,
null
,
{
username
:
u
,
email
:
e
,
password
:
p
});
export
const
getMe
=
(
t
)
=>
request
(
"GET"
,
"/auth/me"
,
t
);
// Chats
export
const
listChats
=
(
t
)
=>
request
(
"GET"
,
"/chats"
,
t
);
export
const
createChat
=
(
t
,
d
=
{})
=>
request
(
"POST"
,
"/chats"
,
t
,
d
);
export
const
updateChat
=
(
t
,
id
,
d
)
=>
request
(
"PUT"
,
`/chats/
${
id
}
`
,
t
,
d
);
export
const
renameChat
=
(
t
,
id
,
title
)
=>
updateChat
(
t
,
id
,
{
title
});
export
const
deleteChat
=
(
t
,
id
)
=>
request
(
"DELETE"
,
`/chats/
${
id
}
`
,
t
);
export
const
getMessages
=
(
t
,
id
)
=>
request
(
"GET"
,
`/chats/
${
id
}
/messages`
,
t
);
export
const
checkGenerating
=
(
t
,
id
)
=>
request
(
"GET"
,
`/chats/
${
id
}
/generating`
,
t
);
export
const
refreshRepoContext
=
(
t
,
id
)
=>
request
(
"POST"
,
`/chats/
${
id
}
/refresh-repo`
,
t
);
export
const
commitFromChat
=
(
t
,
id
,
d
)
=>
request
(
"POST"
,
`/chats/
${
id
}
/commit`
,
t
,
d
);
// Streaming
export
async
function
*
streamMessage
(
token
,
chatId
,
body
,
signal
)
{
const
res
=
await
fetch
(
`
${
BASE
}
/chats/
${
chatId
}
/messages`
,
{
method
:
"POST"
,
headers
:
headers
(
token
),
body
:
JSON
.
stringify
(
body
),
signal
});
if
(
!
res
.
ok
)
{
const
err
=
await
res
.
json
().
catch
(()
=>
({
detail
:
res
.
statusText
}));
throw
new
Error
(
extractError
(
err
,
"Stream failed"
));
}
const
reader
=
res
.
body
.
getReader
();
const
decoder
=
new
TextDecoder
();
let
buffer
=
""
;
while
(
true
)
{
const
{
done
,
value
}
=
await
reader
.
read
();
if
(
done
)
break
;
buffer
+=
decoder
.
decode
(
value
,
{
stream
:
true
});
const
parts
=
buffer
.
split
(
"
\n\n
"
);
buffer
=
parts
.
pop
()
||
""
;
for
(
const
part
of
parts
)
{
const
line
=
part
.
trim
();
if
(
line
.
startsWith
(
"data: "
))
{
try
{
yield
JSON
.
parse
(
line
.
slice
(
6
));
}
catch
{
}
}
}
}
if
(
buffer
.
trim
().
startsWith
(
"data: "
))
{
try
{
yield
JSON
.
parse
(
buffer
.
trim
().
slice
(
6
));
}
catch
{
}
}
}
// Attachments
export
async
function
uploadAttachments
(
t
,
chatId
,
files
)
{
const
form
=
new
FormData
();
for
(
const
f
of
files
)
form
.
append
(
"files"
,
f
);
const
res
=
await
fetch
(
`
${
BASE
}
/chats/
${
chatId
}
/attachments`
,
{
method
:
"POST"
,
headers
:
authHeader
(
t
),
body
:
form
});
if
(
!
res
.
ok
)
{
const
err
=
await
res
.
json
().
catch
(()
=>
({}));
throw
new
Error
(
extractError
(
err
,
"Upload failed"
));
}
return
res
.
json
();
}
export
function
getAttachmentUrl
(
id
)
{
return
`
${
BASE
}
/attachments/
${
id
}
/file`
;
}
export
const
deleteAttachment
=
(
t
,
id
)
=>
request
(
"DELETE"
,
`/attachments/
${
id
}
`
,
t
);
// Knowledge
export
const
listKnowledgeBases
=
(
t
)
=>
request
(
"GET"
,
"/knowledge"
,
t
);
export
const
createKnowledgeBase
=
(
t
,
n
,
d
=
""
)
=>
request
(
"POST"
,
"/knowledge"
,
t
,
{
name
:
n
,
description
:
d
});
export
const
getKnowledgeBase
=
(
t
,
id
)
=>
request
(
"GET"
,
`/knowledge/
${
id
}
`
,
t
);
export
const
updateKnowledgeBase
=
(
t
,
id
,
d
)
=>
request
(
"PUT"
,
`/knowledge/
${
id
}
`
,
t
,
d
);
export
const
deleteKnowledgeBase
=
(
t
,
id
)
=>
request
(
"DELETE"
,
`/knowledge/
${
id
}
`
,
t
);
export
const
listKnowledgeDocuments
=
(
t
,
id
)
=>
request
(
"GET"
,
`/knowledge/
${
id
}
/documents`
,
t
);
export
const
deleteKnowledgeDocument
=
(
t
,
kbId
,
docId
)
=>
request
(
"DELETE"
,
`/knowledge/
${
kbId
}
/documents/
${
docId
}
`
,
t
);
export
async
function
uploadDocuments
(
t
,
kbId
,
files
)
{
const
form
=
new
FormData
();
for
(
const
f
of
files
)
form
.
append
(
"files"
,
f
);
const
res
=
await
fetch
(
`
${
BASE
}
/knowledge/
${
kbId
}
/upload`
,
{
method
:
"POST"
,
headers
:
authHeader
(
t
),
body
:
form
});
if
(
!
res
.
ok
)
{
const
err
=
await
res
.
json
().
catch
(()
=>
({}));
throw
new
Error
(
extractError
(
err
,
"Upload failed"
));
}
return
res
.
json
();
}
export
const
uploadDocument
=
(
t
,
kbId
,
f
)
=>
uploadDocuments
(
t
,
kbId
,
[
f
]);
// Admin
export
const
adminStats
=
(
t
)
=>
request
(
"GET"
,
"/admin/stats"
,
t
);
export
const
adminListUsers
=
(
t
)
=>
request
(
"GET"
,
"/admin/users"
,
t
);
export
const
adminCreateUser
=
(
t
,
d
)
=>
request
(
"POST"
,
"/admin/users"
,
t
,
d
);
export
const
adminUpdateUser
=
(
t
,
id
,
d
)
=>
request
(
"PUT"
,
`/admin/users/
${
id
}
`
,
t
,
d
);
export
const
adminDeleteUser
=
(
t
,
id
)
=>
request
(
"DELETE"
,
`/admin/users/
${
id
}
`
,
t
);
export
const
adminListChats
=
(
t
)
=>
request
(
"GET"
,
"/admin/chats"
,
t
);
// Admin — Permissions
export
const
adminGetUserPermissions
=
(
t
,
uid
)
=>
request
(
"GET"
,
`/admin/users/
${
uid
}
/permissions`
,
t
);
export
const
adminUpdateUserPermissions
=
(
t
,
uid
,
d
)
=>
request
(
"PUT"
,
`/admin/users/
${
uid
}
/permissions`
,
t
,
d
);
export
const
adminGetDefaultPermissions
=
(
t
)
=>
request
(
"GET"
,
"/admin/permissions/defaults"
,
t
);
export
const
adminUpdateDefaultPermissions
=
(
t
,
d
)
=>
request
(
"PUT"
,
"/admin/permissions/defaults"
,
t
,
d
);
export
const
adminApplyDefaults
=
(
t
)
=>
request
(
"POST"
,
"/admin/permissions/apply-defaults"
,
t
);
export
const
adminGetModels
=
(
t
)
=>
request
(
"GET"
,
"/admin/models"
,
t
);
// Code Download
export
async
function
downloadZip
(
t
,
md
,
title
)
{
const
res
=
await
fetch
(
`
${
BASE
}
/files/download-zip`
,
{
method
:
"POST"
,
headers
:
headers
(
t
),
body
:
JSON
.
stringify
({
markdown
:
md
,
title
:
title
||
null
})
});
if
(
!
res
.
ok
)
throw
new
Error
(
"Download failed"
);
const
ct
=
res
.
headers
.
get
(
"content-type"
)
||
""
;
if
(
ct
.
includes
(
"application/zip"
))
{
const
blob
=
await
res
.
blob
();
const
url
=
URL
.
createObjectURL
(
blob
);
const
a
=
document
.
createElement
(
"a"
);
a
.
href
=
url
;
const
raw
=
(
title
||
""
).
trim
();
a
.
download
=
`
${
raw
&&
raw
!==
"New Chat"
?
raw
.
replace
(
/
[^\w\s
-
]
/g
,
""
).
trim
().
replace
(
/
\s
+/g
,
"-"
).
slice
(
0
,
60
)
||
"code"
:
"code"
}
.zip`
;
a
.
click
();
URL
.
revokeObjectURL
(
url
);
}
else
{
const
data
=
await
res
.
json
();
if
(
data
.
error
)
throw
new
Error
(
data
.
error
);
}
}
// Export PPTX / DOCX
export
async
function
exportPptx
(
token
,
markdown
,
title
)
{
const
res
=
await
fetch
(
`
${
BASE
}
/export/pptx`
,
{
method
:
"POST"
,
headers
:
headers
(
token
),
body
:
JSON
.
stringify
({
markdown
,
title
})
});
if
(
!
res
.
ok
)
{
const
err
=
await
res
.
json
().
catch
(()
=>
({}));
throw
new
Error
(
extractError
(
err
,
"PPTX export failed"
));
}
const
blob
=
await
res
.
blob
();
const
url
=
URL
.
createObjectURL
(
blob
);
const
a
=
document
.
createElement
(
"a"
);
a
.
href
=
url
;
const
safe
=
(
title
||
"presentation"
).
replace
(
/
[^\w\s
-
]
/g
,
""
).
trim
().
replace
(
/
\s
+/g
,
"-"
).
slice
(
0
,
50
)
||
"presentation"
;
a
.
download
=
`
${
safe
}
.pptx`
;
a
.
click
();
URL
.
revokeObjectURL
(
url
);
}
export
async
function
exportDocx
(
token
,
markdown
,
title
)
{
const
res
=
await
fetch
(
`
${
BASE
}
/export/docx`
,
{
method
:
"POST"
,
headers
:
headers
(
token
),
body
:
JSON
.
stringify
({
markdown
,
title
})
});
if
(
!
res
.
ok
)
{
const
err
=
await
res
.
json
().
catch
(()
=>
({}));
throw
new
Error
(
extractError
(
err
,
"DOCX export failed"
));
}
const
blob
=
await
res
.
blob
();
const
url
=
URL
.
createObjectURL
(
blob
);
const
a
=
document
.
createElement
(
"a"
);
a
.
href
=
url
;
const
safe
=
(
title
||
"document"
).
replace
(
/
[^\w\s
-
]
/g
,
""
).
trim
().
replace
(
/
\s
+/g
,
"-"
).
slice
(
0
,
50
)
||
"document"
;
a
.
download
=
`
${
safe
}
.docx`
;
a
.
click
();
URL
.
revokeObjectURL
(
url
);
}
// Utilities
const
CODE_BLOCK_RE
=
/```
(\S
*
?)(?:
:
(\S
+
?))?\s
*
?\n([\s\S]
*
?)
```/g
;
export
function
extractCodeBlocks
(
md
)
{
if
(
!
md
)
return
[];
const
blocks
=
[];
let
m
;
const
re
=
new
RegExp
(
CODE_BLOCK_RE
.
source
,
"g"
);
while
((
m
=
re
.
exec
(
md
))
!==
null
)
{
const
lang
=
(
m
[
1
]
||
"text"
).
toLowerCase
();
const
fn
=
m
[
2
]
||
null
;
const
code
=
(
m
[
3
]
||
""
).
trim
();
if
(
code
)
blocks
.
push
({
language
:
lang
,
filename
:
fn
,
code
});
}
return
blocks
;
}
// GitLab
export
const
gitlabGetSettings
=
(
t
)
=>
request
(
"GET"
,
"/gitlab/settings"
,
t
);
export
const
gitlabUpdateSettings
=
(
t
,
d
)
=>
request
(
"PUT"
,
"/gitlab/settings"
,
t
,
d
);
export
const
gitlabTestConnection
=
(
t
)
=>
request
(
"POST"
,
"/gitlab/test-connection"
,
t
);
export
const
gitlabSearchProjects
=
(
t
,
s
,
o
)
=>
request
(
"GET"
,
`/gitlab/projects?search=
${
encodeURIComponent
(
s
||
""
)}
&owned=
${
o
||
false
}
`
,
t
);
export
const
gitlabCreateProject
=
(
t
,
d
)
=>
request
(
"POST"
,
"/gitlab/projects"
,
t
,
d
);
export
const
gitlabListRepos
=
(
t
)
=>
request
(
"GET"
,
"/gitlab/repos"
,
t
);
export
const
gitlabLinkRepo
=
(
t
,
pid
)
=>
request
(
"POST"
,
"/gitlab/repos"
,
t
,
{
gitlab_project_id
:
pid
});
export
const
gitlabUnlinkRepo
=
(
t
,
id
)
=>
request
(
"DELETE"
,
`/gitlab/repos/
${
id
}
`
,
t
);
export
const
gitlabGetTree
=
(
t
,
id
,
p
,
r
)
=>
request
(
"GET"
,
`/gitlab/repos/
${
id
}
/tree?path=
${
encodeURIComponent
(
p
||
""
)}
&ref=
${
encodeURIComponent
(
r
||
""
)}
`
,
t
);
export
const
gitlabGetFile
=
(
t
,
id
,
p
,
r
)
=>
request
(
"GET"
,
`/gitlab/repos/
${
id
}
/file?path=
${
encodeURIComponent
(
p
)}
&ref=
${
encodeURIComponent
(
r
||
""
)}
`
,
t
);
export
const
gitlabGetBranches
=
(
t
,
id
)
=>
request
(
"GET"
,
`/gitlab/repos/
${
id
}
/branches`
,
t
);
export
const
gitlabCreateBranch
=
(
t
,
id
,
d
)
=>
request
(
"POST"
,
`/gitlab/repos/
${
id
}
/branches`
,
t
,
d
);
export
const
gitlabCommit
=
(
t
,
id
,
d
)
=>
request
(
"POST"
,
`/gitlab/repos/
${
id
}
/commit`
,
t
,
d
);
export
const
gitlabCommitSingle
=
(
t
,
id
,
d
)
=>
request
(
"POST"
,
`/gitlab/repos/
${
id
}
/commit-single`
,
t
,
d
);
export
const
gitlabCreateMR
=
(
t
,
id
,
d
)
=>
request
(
"POST"
,
`/gitlab/repos/
${
id
}
/merge-request`
,
t
,
d
);
export
const
gitlabReanalyzeRepo
=
(
t
,
id
)
=>
request
(
"POST"
,
`/gitlab/repos/
${
id
}
/analyze`
,
t
);
export
const
gitlabGetRepoMap
=
(
t
,
id
)
=>
request
(
"GET"
,
`/gitlab/repos/
${
id
}
/map`
,
t
);
export
const
gitlabListActions
=
(
t
,
s
)
=>
request
(
"GET"
,
`/gitlab/actions?status=
${
s
||
"pending"
}
`
,
t
);
export
const
gitlabCreateAction
=
(
t
,
d
)
=>
request
(
"POST"
,
"/gitlab/actions"
,
t
,
d
);
export
const
gitlabApproveAction
=
(
t
,
id
)
=>
request
(
"POST"
,
`/gitlab/actions/
${
id
}
/approve`
,
t
);
export
const
gitlabRejectAction
=
(
t
,
id
)
=>
request
(
"POST"
,
`/gitlab/actions/
${
id
}
/reject`
,
t
);
\ No newline at end of file
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