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
e636c018
Commit
e636c018
authored
Mar 19, 2026
by
Mahmoud Aglan
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
jjjjjj
parent
4bc6c1f8
Changes
8
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
2802 additions
and
2141 deletions
+2802
-2141
FULL_CODEBASE.txt
FULL_CODEBASE.txt
+2133
-2041
knowledge_routes.py
backend/routes/knowledge_routes.py
+116
-13
rag_service.py
backend/services/rag_service.py
+35
-1
fix-and-deploy.sh
fix-and-deploy.sh
+0
-1
App.jsx
frontend/src/App.jsx
+2
-0
api.js
frontend/src/api.js
+75
-13
ChatPage.jsx
frontend/src/pages/ChatPage.jsx
+56
-72
KnowledgePage.jsx
frontend/src/pages/KnowledgePage.jsx
+385
-0
No files found.
FULL_CODEBASE.txt
View file @
e636c018
This source diff could not be displayed because it is too large. You can
view the blob
instead.
backend/routes/knowledge_routes.py
View file @
e636c018
"""
"""
Knowledge base management and document upload (supports multiple files at once).
Knowledge base management and document upload.
Full CRUD for knowledge bases AND their individual documents.
"""
"""
from
pydantic
import
BaseModel
from
pydantic
import
BaseModel
...
@@ -22,6 +23,15 @@ class CreateKBBody(BaseModel):
...
@@ -22,6 +23,15 @@ class CreateKBBody(BaseModel):
description
:
str
=
""
description
:
str
=
""
class
UpdateKBBody
(
BaseModel
):
name
:
Optional
[
str
]
=
None
description
:
Optional
[
str
]
=
None
# ═══════════════════════════════════════════════════
# Knowledge Base CRUD
# ═══════════════════════════════════════════════════
@
router
.
get
(
""
)
@
router
.
get
(
""
)
def
list_knowledge_bases
(
user
:
User
=
Depends
(
get_current_user
),
db
:
Session
=
Depends
(
get_db
)):
def
list_knowledge_bases
(
user
:
User
=
Depends
(
get_current_user
),
db
:
Session
=
Depends
(
get_db
)):
kbs
=
db
.
query
(
KnowledgeBase
)
.
filter
(
kbs
=
db
.
query
(
KnowledgeBase
)
.
filter
(
...
@@ -43,22 +53,35 @@ def create_kb(body: CreateKBBody, user: User = Depends(get_current_user), db: Se
...
@@ -43,22 +53,35 @@ def create_kb(body: CreateKBBody, user: User = Depends(get_current_user), db: Se
@
router
.
get
(
"/{kb_id}"
)
@
router
.
get
(
"/{kb_id}"
)
def
get_kb
(
kb_id
:
str
,
user
:
User
=
Depends
(
get_current_user
),
db
:
Session
=
Depends
(
get_db
)):
def
get_kb
(
kb_id
:
str
,
user
:
User
=
Depends
(
get_current_user
),
db
:
Session
=
Depends
(
get_db
)):
kb
=
_get_kb
(
kb_id
,
user
,
db
)
kb
=
_get_kb
(
kb_id
,
user
,
db
)
docs
=
db
.
query
(
KnowledgeDocument
)
.
filter
(
KnowledgeDocument
.
knowledge_base_id
==
kb_id
)
.
all
()
docs
=
(
db
.
query
(
KnowledgeDocument
)
.
filter
(
KnowledgeDocument
.
knowledge_base_id
==
kb_id
)
.
order_by
(
KnowledgeDocument
.
created_at
.
desc
())
.
all
()
)
return
{
return
{
**
_kb_dict
(
kb
),
**
_kb_dict
(
kb
),
"documents"
:
[
"documents"
:
[
_doc_dict
(
d
)
for
d
in
docs
],
{
"id"
:
d
.
id
,
"filename"
:
d
.
filename
,
"file_size"
:
d
.
file_size
,
"chunk_count"
:
d
.
chunk_count
,
"created_at"
:
str
(
d
.
created_at
),
}
for
d
in
docs
],
}
}
@
router
.
put
(
"/{kb_id}"
)
def
update_kb
(
kb_id
:
str
,
body
:
UpdateKBBody
,
user
:
User
=
Depends
(
get_current_user
),
db
:
Session
=
Depends
(
get_db
),
):
kb
=
_get_kb
(
kb_id
,
user
,
db
)
if
body
.
name
is
not
None
:
kb
.
name
=
body
.
name
if
body
.
description
is
not
None
:
kb
.
description
=
body
.
description
db
.
commit
()
db
.
refresh
(
kb
)
return
_kb_dict
(
kb
)
@
router
.
delete
(
"/{kb_id}"
)
@
router
.
delete
(
"/{kb_id}"
)
def
delete_kb
(
kb_id
:
str
,
user
:
User
=
Depends
(
get_current_user
),
db
:
Session
=
Depends
(
get_db
)):
def
delete_kb
(
kb_id
:
str
,
user
:
User
=
Depends
(
get_current_user
),
db
:
Session
=
Depends
(
get_db
)):
kb
=
_get_kb
(
kb_id
,
user
,
db
)
kb
=
_get_kb
(
kb_id
,
user
,
db
)
...
@@ -72,6 +95,72 @@ def delete_kb(kb_id: str, user: User = Depends(get_current_user), db: Session =
...
@@ -72,6 +95,72 @@ def delete_kb(kb_id: str, user: User = Depends(get_current_user), db: Session =
return
{
"ok"
:
True
}
return
{
"ok"
:
True
}
# ═══════════════════════════════════════════════════
# Document Management
# ═══════════════════════════════════════════════════
@
router
.
get
(
"/{kb_id}/documents"
)
def
list_documents
(
kb_id
:
str
,
user
:
User
=
Depends
(
get_current_user
),
db
:
Session
=
Depends
(
get_db
),
):
"""List all documents in a knowledge base."""
_get_kb
(
kb_id
,
user
,
db
)
docs
=
(
db
.
query
(
KnowledgeDocument
)
.
filter
(
KnowledgeDocument
.
knowledge_base_id
==
kb_id
)
.
order_by
(
KnowledgeDocument
.
created_at
.
desc
())
.
all
()
)
return
[
_doc_dict
(
d
)
for
d
in
docs
]
@
router
.
delete
(
"/{kb_id}/documents/{doc_id}"
)
def
delete_document
(
kb_id
:
str
,
doc_id
:
str
,
user
:
User
=
Depends
(
get_current_user
),
db
:
Session
=
Depends
(
get_db
),
):
"""Delete a single document from a knowledge base, including its vector chunks."""
kb
=
_get_kb
(
kb_id
,
user
,
db
)
doc
=
(
db
.
query
(
KnowledgeDocument
)
.
filter
(
KnowledgeDocument
.
id
==
doc_id
,
KnowledgeDocument
.
knowledge_base_id
==
kb_id
)
.
first
()
)
if
not
doc
:
raise
HTTPException
(
404
,
"Document not found"
)
# Remove vector chunks from ChromaDB
try
:
removed_count
=
rag_service
.
delete_document_chunks
(
kb_id
,
doc
.
filename
)
except
Exception
:
removed_count
=
0
# Update KB aggregate stats
kb
.
document_count
=
max
((
kb
.
document_count
or
0
)
-
1
,
0
)
kb
.
chunk_count
=
max
((
kb
.
chunk_count
or
0
)
-
(
doc
.
chunk_count
or
0
),
0
)
# Estimate character reduction (rough: chunk_count * avg_chunk_size)
estimated_chars
=
(
doc
.
chunk_count
or
0
)
*
2000
kb
.
total_characters
=
max
((
kb
.
total_characters
or
0
)
-
estimated_chars
,
0
)
db
.
delete
(
doc
)
db
.
commit
()
return
{
"ok"
:
True
,
"chunks_removed"
:
removed_count
,
"document_id"
:
doc_id
,
"filename"
:
doc
.
filename
,
}
# ═══════════════════════════════════════════════════
# Upload Documents
# ═══════════════════════════════════════════════════
@
router
.
post
(
"/{kb_id}/upload"
)
@
router
.
post
(
"/{kb_id}/upload"
)
async
def
upload_documents
(
async
def
upload_documents
(
kb_id
:
str
,
kb_id
:
str
,
...
@@ -150,6 +239,10 @@ async def upload_documents(
...
@@ -150,6 +239,10 @@ async def upload_documents(
}
}
# ═══════════════════════════════════════════════════
# Helpers
# ═══════════════════════════════════════════════════
def
_get_kb
(
kb_id
:
str
,
user
:
User
,
db
:
Session
)
->
KnowledgeBase
:
def
_get_kb
(
kb_id
:
str
,
user
:
User
,
db
:
Session
)
->
KnowledgeBase
:
kb
=
db
.
query
(
KnowledgeBase
)
.
filter
(
KnowledgeBase
.
id
==
kb_id
)
.
first
()
kb
=
db
.
query
(
KnowledgeBase
)
.
filter
(
KnowledgeBase
.
id
==
kb_id
)
.
first
()
if
not
kb
:
if
not
kb
:
...
@@ -198,10 +291,20 @@ def _kb_dict(kb: KnowledgeBase) -> dict:
...
@@ -198,10 +291,20 @@ def _kb_dict(kb: KnowledgeBase) -> dict:
return
{
return
{
"id"
:
kb
.
id
,
"id"
:
kb
.
id
,
"name"
:
kb
.
name
,
"name"
:
kb
.
name
,
"description"
:
kb
.
description
,
"description"
:
kb
.
description
or
""
,
"document_count"
:
kb
.
document_count
or
0
,
"document_count"
:
kb
.
document_count
or
0
,
"chunk_count"
:
kb
.
chunk_count
or
0
,
"chunk_count"
:
kb
.
chunk_count
or
0
,
"total_characters"
:
kb
.
total_characters
or
0
,
"total_characters"
:
kb
.
total_characters
or
0
,
"estimated_tokens"
:
(
kb
.
total_characters
or
0
)
//
4
,
"estimated_tokens"
:
(
kb
.
total_characters
or
0
)
//
4
,
"created_at"
:
str
(
kb
.
created_at
),
"created_at"
:
str
(
kb
.
created_at
),
}
def
_doc_dict
(
d
:
KnowledgeDocument
)
->
dict
:
return
{
"id"
:
d
.
id
,
"filename"
:
d
.
filename
,
"file_size"
:
d
.
file_size
or
0
,
"chunk_count"
:
d
.
chunk_count
or
0
,
"created_at"
:
str
(
d
.
created_at
),
}
}
\ No newline at end of file
backend/services/rag_service.py
View file @
e636c018
"""
"""
RAG (Retrieval-Augmented Generation) via ChromaDB.
RAG (Retrieval-Augmented Generation) via ChromaDB.
Each knowledge base maps to a ChromaDB collection.
Each knowledge base maps to a ChromaDB collection.
Supports document-level deletion by filename metadata.
"""
"""
import
os
import
os
...
@@ -39,7 +40,6 @@ def add_documents(
...
@@ -39,7 +40,6 @@ def add_documents(
):
):
col
=
_chroma_client
.
get_or_create_collection
(
name
=
_col_name
(
collection_id
))
col
=
_chroma_client
.
get_or_create_collection
(
name
=
_col_name
(
collection_id
))
ids
=
[
str
(
uuid4
())
for
_
in
documents
]
ids
=
[
str
(
uuid4
())
for
_
in
documents
]
# ChromaDB handles batching internally; we chunk to stay under its limits
batch_size
=
500
batch_size
=
500
for
i
in
range
(
0
,
len
(
documents
),
batch_size
):
for
i
in
range
(
0
,
len
(
documents
),
batch_size
):
batch_docs
=
documents
[
i
:
i
+
batch_size
]
batch_docs
=
documents
[
i
:
i
+
batch_size
]
...
@@ -48,6 +48,40 @@ def add_documents(
...
@@ -48,6 +48,40 @@ def add_documents(
col
.
add
(
documents
=
batch_docs
,
ids
=
batch_ids
,
metadatas
=
batch_meta
)
col
.
add
(
documents
=
batch_docs
,
ids
=
batch_ids
,
metadatas
=
batch_meta
)
def
delete_document_chunks
(
collection_id
:
str
,
filename
:
str
)
->
int
:
"""
Delete all vector chunks belonging to a specific document (by filename).
Returns the number of chunks removed.
"""
try
:
col
=
_chroma_client
.
get_collection
(
name
=
_col_name
(
collection_id
))
except
Exception
:
return
0
if
col
.
count
()
==
0
:
return
0
# Fetch all chunk IDs that match this filename
try
:
results
=
col
.
get
(
where
=
{
"filename"
:
filename
},
include
=
[],
# We only need IDs
)
chunk_ids
=
results
.
get
(
"ids"
,
[])
if
not
chunk_ids
:
return
0
# Delete in batches (ChromaDB can handle large deletes but let's be safe)
batch_size
=
500
for
i
in
range
(
0
,
len
(
chunk_ids
),
batch_size
):
batch
=
chunk_ids
[
i
:
i
+
batch_size
]
col
.
delete
(
ids
=
batch
)
return
len
(
chunk_ids
)
except
Exception
:
return
0
def
query
(
collection_id
:
str
,
query_text
:
str
,
n_results
:
int
=
8
)
->
str
|
None
:
def
query
(
collection_id
:
str
,
query_text
:
str
,
n_results
:
int
=
8
)
->
str
|
None
:
"""
"""
Return a formatted string of the top matching chunks,
Return a formatted string of the top matching chunks,
...
...
fix-and-deploy.sh
View file @
e636c018
...
@@ -256,7 +256,6 @@ tar cf "$TARBALL" \
...
@@ -256,7 +256,6 @@ tar cf "$TARBALL" \
--exclude
=
'.env.local'
\
--exclude
=
'.env.local'
\
--exclude
=
'.idea'
\
--exclude
=
'.idea'
\
--exclude
=
'.vscode'
\
--exclude
=
'.vscode'
\
--exclude
=
'./main.py'
\
--exclude
=
'create-project.ps1'
\
--exclude
=
'create-project.ps1'
\
--exclude
=
'*.sh'
\
--exclude
=
'*.sh'
\
.
.
...
...
frontend/src/App.jsx
View file @
e636c018
...
@@ -6,6 +6,7 @@ import * as streamManager from "./streamManager";
...
@@ -6,6 +6,7 @@ import * as streamManager from "./streamManager";
import
LoginPage
from
"./pages/LoginPage"
;
import
LoginPage
from
"./pages/LoginPage"
;
import
ChatPage
from
"./pages/ChatPage"
;
import
ChatPage
from
"./pages/ChatPage"
;
import
AdminPage
from
"./pages/AdminPage"
;
import
AdminPage
from
"./pages/AdminPage"
;
import
KnowledgePage
from
"./pages/KnowledgePage"
;
import
{
Flame
}
from
"lucide-react"
;
import
{
Flame
}
from
"lucide-react"
;
export
default
function
App
()
{
export
default
function
App
()
{
...
@@ -58,6 +59,7 @@ export default function App() {
...
@@ -58,6 +59,7 @@ export default function App() {
return
(
return
(
<
Routes
>
<
Routes
>
<
Route
path=
"/admin"
element=
{
<
AdminPage
/>
}
/>
<
Route
path=
"/admin"
element=
{
<
AdminPage
/>
}
/>
<
Route
path=
"/knowledge"
element=
{
<
KnowledgePage
/>
}
/>
<
Route
path=
"/*"
element=
{
<
ChatPage
/>
}
/>
<
Route
path=
"/*"
element=
{
<
ChatPage
/>
}
/>
</
Routes
>
</
Routes
>
);
);
...
...
frontend/src/api.js
View file @
e636c018
...
@@ -21,6 +21,10 @@ async function request(method, path, token, body) {
...
@@ -21,6 +21,10 @@ async function request(method, path, token, body) {
return
res
.
json
();
return
res
.
json
();
}
}
// ═══════════════════════════════════════════════════
// Auth
// ═══════════════════════════════════════════════════
export
const
login
=
(
username
,
password
)
=>
export
const
login
=
(
username
,
password
)
=>
request
(
"POST"
,
"/auth/login"
,
null
,
{
username
,
password
});
request
(
"POST"
,
"/auth/login"
,
null
,
{
username
,
password
});
...
@@ -29,6 +33,10 @@ export const register = (username, email, password) =>
...
@@ -29,6 +33,10 @@ export const register = (username, email, password) =>
export
const
getMe
=
(
token
)
=>
request
(
"GET"
,
"/auth/me"
,
token
);
export
const
getMe
=
(
token
)
=>
request
(
"GET"
,
"/auth/me"
,
token
);
// ═══════════════════════════════════════════════════
// Chats
// ═══════════════════════════════════════════════════
export
const
listChats
=
(
token
)
=>
request
(
"GET"
,
"/chats"
,
token
);
export
const
listChats
=
(
token
)
=>
request
(
"GET"
,
"/chats"
,
token
);
export
const
createChat
=
(
token
,
data
=
{})
=>
request
(
"POST"
,
"/chats"
,
token
,
data
);
export
const
createChat
=
(
token
,
data
=
{})
=>
request
(
"POST"
,
"/chats"
,
token
,
data
);
...
@@ -48,10 +56,16 @@ export const getMessages = (token, chatId) =>
...
@@ -48,10 +56,16 @@ export const getMessages = (token, chatId) =>
export
const
checkGenerating
=
(
token
,
chatId
)
=>
export
const
checkGenerating
=
(
token
,
chatId
)
=>
request
(
"GET"
,
`/chats/
${
chatId
}
/generating`
,
token
);
request
(
"GET"
,
`/chats/
${
chatId
}
/generating`
,
token
);
// ═══════════════════════════════════════════════════
// Streaming
// ═══════════════════════════════════════════════════
export
async
function
*
streamMessage
(
token
,
chatId
,
body
,
signal
)
{
export
async
function
*
streamMessage
(
token
,
chatId
,
body
,
signal
)
{
const
res
=
await
fetch
(
`
${
BASE
}
/chats/
${
chatId
}
/messages`
,
{
const
res
=
await
fetch
(
`
${
BASE
}
/chats/
${
chatId
}
/messages`
,
{
method
:
"POST"
,
headers
:
headers
(
token
),
method
:
"POST"
,
body
:
JSON
.
stringify
(
body
),
signal
,
headers
:
headers
(
token
),
body
:
JSON
.
stringify
(
body
),
signal
,
});
});
if
(
!
res
.
ok
)
{
if
(
!
res
.
ok
)
{
const
err
=
await
res
.
json
().
catch
(()
=>
({
detail
:
res
.
statusText
}));
const
err
=
await
res
.
json
().
catch
(()
=>
({
detail
:
res
.
statusText
}));
...
@@ -69,20 +83,34 @@ export async function* streamMessage(token, chatId, body, signal) {
...
@@ -69,20 +83,34 @@ export async function* streamMessage(token, chatId, body, signal) {
for
(
const
part
of
parts
)
{
for
(
const
part
of
parts
)
{
const
line
=
part
.
trim
();
const
line
=
part
.
trim
();
if
(
line
.
startsWith
(
"data: "
))
{
if
(
line
.
startsWith
(
"data: "
))
{
try
{
yield
JSON
.
parse
(
line
.
slice
(
6
));
}
catch
{
/* skip */
}
try
{
yield
JSON
.
parse
(
line
.
slice
(
6
));
}
catch
{
/* skip */
}
}
}
}
}
}
}
if
(
buffer
.
trim
().
startsWith
(
"data: "
))
{
if
(
buffer
.
trim
().
startsWith
(
"data: "
))
{
try
{
yield
JSON
.
parse
(
buffer
.
trim
().
slice
(
6
));
}
catch
{
/* skip */
}
try
{
yield
JSON
.
parse
(
buffer
.
trim
().
slice
(
6
));
}
catch
{
/* skip */
}
}
}
}
}
// ═══════════════════════════════════════════════════
// Chat Attachments
// ═══════════════════════════════════════════════════
export
async
function
uploadAttachments
(
token
,
chatId
,
files
)
{
export
async
function
uploadAttachments
(
token
,
chatId
,
files
)
{
const
form
=
new
FormData
();
const
form
=
new
FormData
();
for
(
const
file
of
files
)
form
.
append
(
"files"
,
file
);
for
(
const
file
of
files
)
form
.
append
(
"files"
,
file
);
const
res
=
await
fetch
(
`
${
BASE
}
/chats/
${
chatId
}
/attachments`
,
{
const
res
=
await
fetch
(
`
${
BASE
}
/chats/
${
chatId
}
/attachments`
,
{
method
:
"POST"
,
headers
:
authHeader
(
token
),
body
:
form
,
method
:
"POST"
,
headers
:
authHeader
(
token
),
body
:
form
,
});
});
if
(
!
res
.
ok
)
{
if
(
!
res
.
ok
)
{
const
err
=
await
res
.
json
().
catch
(()
=>
({}));
const
err
=
await
res
.
json
().
catch
(()
=>
({}));
...
@@ -98,7 +126,12 @@ export function getAttachmentUrl(attachmentId) {
...
@@ -98,7 +126,12 @@ export function getAttachmentUrl(attachmentId) {
export
const
deleteAttachment
=
(
token
,
attachmentId
)
=>
export
const
deleteAttachment
=
(
token
,
attachmentId
)
=>
request
(
"DELETE"
,
`/attachments/
${
attachmentId
}
`
,
token
);
request
(
"DELETE"
,
`/attachments/
${
attachmentId
}
`
,
token
);
export
const
listKnowledgeBases
=
(
token
)
=>
request
(
"GET"
,
"/knowledge"
,
token
);
// ═══════════════════════════════════════════════════
// Knowledge Bases
// ═══════════════════════════════════════════════════
export
const
listKnowledgeBases
=
(
token
)
=>
request
(
"GET"
,
"/knowledge"
,
token
);
export
const
createKnowledgeBase
=
(
token
,
name
,
description
=
""
)
=>
export
const
createKnowledgeBase
=
(
token
,
name
,
description
=
""
)
=>
request
(
"POST"
,
"/knowledge"
,
token
,
{
name
,
description
});
request
(
"POST"
,
"/knowledge"
,
token
,
{
name
,
description
});
...
@@ -106,14 +139,29 @@ export const createKnowledgeBase = (token, name, description = "") =>
...
@@ -106,14 +139,29 @@ export const createKnowledgeBase = (token, name, description = "") =>
export
const
getKnowledgeBase
=
(
token
,
kbId
)
=>
export
const
getKnowledgeBase
=
(
token
,
kbId
)
=>
request
(
"GET"
,
`/knowledge/
${
kbId
}
`
,
token
);
request
(
"GET"
,
`/knowledge/
${
kbId
}
`
,
token
);
export
const
updateKnowledgeBase
=
(
token
,
kbId
,
data
)
=>
request
(
"PUT"
,
`/knowledge/
${
kbId
}
`
,
token
,
data
);
export
const
deleteKnowledgeBase
=
(
token
,
kbId
)
=>
export
const
deleteKnowledgeBase
=
(
token
,
kbId
)
=>
request
(
"DELETE"
,
`/knowledge/
${
kbId
}
`
,
token
);
request
(
"DELETE"
,
`/knowledge/
${
kbId
}
`
,
token
);
// ═══════════════════════════════════════════════════
// Knowledge Base Documents
// ═══════════════════════════════════════════════════
export
const
listKnowledgeDocuments
=
(
token
,
kbId
)
=>
request
(
"GET"
,
`/knowledge/
${
kbId
}
/documents`
,
token
);
export
const
deleteKnowledgeDocument
=
(
token
,
kbId
,
docId
)
=>
request
(
"DELETE"
,
`/knowledge/
${
kbId
}
/documents/
${
docId
}
`
,
token
);
export
async
function
uploadDocuments
(
token
,
kbId
,
files
)
{
export
async
function
uploadDocuments
(
token
,
kbId
,
files
)
{
const
form
=
new
FormData
();
const
form
=
new
FormData
();
for
(
const
file
of
files
)
form
.
append
(
"files"
,
file
);
for
(
const
file
of
files
)
form
.
append
(
"files"
,
file
);
const
res
=
await
fetch
(
`
${
BASE
}
/knowledge/
${
kbId
}
/upload`
,
{
const
res
=
await
fetch
(
`
${
BASE
}
/knowledge/
${
kbId
}
/upload`
,
{
method
:
"POST"
,
headers
:
authHeader
(
token
),
body
:
form
,
method
:
"POST"
,
headers
:
authHeader
(
token
),
body
:
form
,
});
});
if
(
!
res
.
ok
)
{
if
(
!
res
.
ok
)
{
const
err
=
await
res
.
json
().
catch
(()
=>
({}));
const
err
=
await
res
.
json
().
catch
(()
=>
({}));
...
@@ -125,16 +173,30 @@ export async function uploadDocuments(token, kbId, files) {
...
@@ -125,16 +173,30 @@ export async function uploadDocuments(token, kbId, files) {
export
const
uploadDocument
=
(
token
,
kbId
,
file
)
=>
export
const
uploadDocument
=
(
token
,
kbId
,
file
)
=>
uploadDocuments
(
token
,
kbId
,
[
file
]);
uploadDocuments
(
token
,
kbId
,
[
file
]);
// ═══════════════════════════════════════════════════
// Admin
// ═══════════════════════════════════════════════════
export
const
adminStats
=
(
token
)
=>
request
(
"GET"
,
"/admin/stats"
,
token
);
export
const
adminStats
=
(
token
)
=>
request
(
"GET"
,
"/admin/stats"
,
token
);
export
const
adminListUsers
=
(
token
)
=>
request
(
"GET"
,
"/admin/users"
,
token
);
export
const
adminListUsers
=
(
token
)
=>
export
const
adminCreateUser
=
(
token
,
data
)
=>
request
(
"POST"
,
"/admin/users"
,
token
,
data
);
request
(
"GET"
,
"/admin/users"
,
token
);
export
const
adminUpdateUser
=
(
token
,
userId
,
data
)
=>
request
(
"PUT"
,
`/admin/users/
${
userId
}
`
,
token
,
data
);
export
const
adminCreateUser
=
(
token
,
data
)
=>
export
const
adminDeleteUser
=
(
token
,
userId
)
=>
request
(
"DELETE"
,
`/admin/users/
${
userId
}
`
,
token
);
request
(
"POST"
,
"/admin/users"
,
token
,
data
);
export
const
adminListChats
=
(
token
)
=>
request
(
"GET"
,
"/admin/chats"
,
token
);
export
const
adminUpdateUser
=
(
token
,
userId
,
data
)
=>
request
(
"PUT"
,
`/admin/users/
${
userId
}
`
,
token
,
data
);
export
const
adminDeleteUser
=
(
token
,
userId
)
=>
request
(
"DELETE"
,
`/admin/users/
${
userId
}
`
,
token
);
export
const
adminListChats
=
(
token
)
=>
request
(
"GET"
,
"/admin/chats"
,
token
);
// ═══════════════════════════════════════════════════
// Code Download
// ═══════════════════════════════════════════════════
export
async
function
downloadZip
(
token
,
markdown
)
{
export
async
function
downloadZip
(
token
,
markdown
)
{
const
res
=
await
fetch
(
`
${
BASE
}
/files/download-zip`
,
{
const
res
=
await
fetch
(
`
${
BASE
}
/files/download-zip`
,
{
method
:
"POST"
,
headers
:
headers
(
token
),
method
:
"POST"
,
headers
:
headers
(
token
),
body
:
JSON
.
stringify
({
markdown
}),
body
:
JSON
.
stringify
({
markdown
}),
});
});
if
(
!
res
.
ok
)
throw
new
Error
(
"Download failed"
);
if
(
!
res
.
ok
)
throw
new
Error
(
"Download failed"
);
...
...
frontend/src/pages/ChatPage.jsx
View file @
e636c018
import
React
,
{
useEffect
,
useState
}
from
"react"
;
import
React
,
{
useEffect
,
useState
}
from
"react"
;
import
{
useApp
}
from
"../store"
;
import
{
useApp
}
from
"../store"
;
import
{
listChats
,
createChat
,
checkGenerating
}
from
"../api"
;
import
{
listChats
,
createChat
}
from
"../api"
;
import
*
as
streamManager
from
"../streamManager"
;
import
Sidebar
from
"../components/Sidebar"
;
import
Sidebar
from
"../components/Sidebar"
;
import
ChatView
from
"../components/ChatView"
;
import
ChatView
from
"../components/ChatView"
;
import
{
Flame
,
Menu
,
Plus
,
MessageSquare
}
from
"lucide-react"
;
import
{
Flame
,
BookOpen
,
Shield
}
from
"lucide-react"
;
import
{
useNavigate
}
from
"react-router-dom"
;
export
default
function
ChatPage
()
{
export
default
function
ChatPage
()
{
const
{
state
,
dispatch
}
=
useApp
();
const
{
state
,
dispatch
}
=
useApp
();
const
navigate
=
useNavigate
();
const
[
activeChatId
,
setActiveChatId
]
=
useState
(
null
);
const
[
activeChatId
,
setActiveChatId
]
=
useState
(
null
);
const
[
sidebarOpen
,
setSidebarOpen
]
=
useState
(
false
);
useEffect
(()
=>
{
useEffect
(()
=>
{
(
async
()
=>
{
(
async
()
=>
{
try
{
try
{
const
chats
=
await
listChats
(
state
.
token
);
const
chats
=
await
listChats
(
state
.
token
);
dispatch
({
type
:
"SET_CHATS"
,
chats
});
dispatch
({
type
:
"SET_CHATS"
,
chats
});
if
(
chats
.
length
&&
!
activeChatId
)
{
if
(
chats
.
length
>
0
&&
!
activeChatId
)
{
const
firstId
=
chats
[
0
].
id
;
setActiveChatId
(
chats
[
0
].
id
);
setActiveChatId
(
firstId
);
// Check if background generation is active
try
{
const
{
active
}
=
await
checkGenerating
(
state
.
token
,
firstId
);
if
(
active
)
streamManager
.
reconnectStream
({
token
:
state
.
token
,
chatId
:
firstId
});
}
catch
{
/* ignore */
}
}
}
}
catch
{
/* ignore */
}
}
catch
{
}
})();
})();
},
[
state
.
token
,
dispatch
]);
},
[
state
.
token
,
dispatch
]);
...
@@ -33,84 +29,72 @@ export default function ChatPage() {
...
@@ -33,84 +29,72 @@ export default function ChatPage() {
const
chat
=
await
createChat
(
state
.
token
);
const
chat
=
await
createChat
(
state
.
token
);
dispatch
({
type
:
"ADD_CHAT"
,
chat
});
dispatch
({
type
:
"ADD_CHAT"
,
chat
});
setActiveChatId
(
chat
.
id
);
setActiveChatId
(
chat
.
id
);
dispatch
({
type
:
"SET_SIDEBAR_OPEN"
,
open
:
false
}
);
setSidebarOpen
(
false
);
}
catch
{
/* ignore */
}
}
catch
{
}
}
}
async
function
handleSelectChat
(
id
)
{
function
handleSelectChat
(
chatId
)
{
setActiveChatId
(
id
);
setActiveChatId
(
chatId
);
dispatch
({
type
:
"SET_SIDEBAR_OPEN"
,
open
:
false
});
setSidebarOpen
(
false
);
// Check for active background generation on this chat
try
{
const
{
active
}
=
await
checkGenerating
(
state
.
token
,
id
);
if
(
active
&&
!
streamManager
.
isStreaming
(
id
))
{
streamManager
.
reconnectStream
({
token
:
state
.
token
,
chatId
:
id
});
}
}
catch
{
/* ignore */
}
}
}
return
(
return
(
<
div
className=
"h-dvh flex overflow-hidden bg-anton-bg"
>
<
div
className=
"h-dvh flex bg-anton-bg text-anton-text overflow-hidden"
>
{
/* Mobile overlay */
}
{
state
.
sidebarOpen
&&
(
<
div
className=
"fixed inset-0 bg-black/60 z-30 lg:hidden"
onClick=
{
()
=>
dispatch
({
type
:
"SET_SIDEBAR_OPEN"
,
open
:
false
})
}
/>
)
}
{
/* Sidebar */
}
{
/* Sidebar */
}
<
div
className=
{
`
<
Sidebar
fixed inset-y-0 left-0 z-40 w-72 transform transition-transform duration-300 ease-in-out
activeChatId=
{
activeChatId
}
lg:relative lg:translate-x-0 lg:w-72 lg:shrink-0
onSelectChat=
{
handleSelectChat
}
${state.sidebarOpen ? "translate-x-0" : "-translate-x-full"}
onNewChat=
{
handleNewChat
}
`
}
>
isOpen=
{
sidebarOpen
}
<
Sidebar
onClose=
{
()
=>
setSidebarOpen
(
false
)
}
activeChatId=
{
activeChatId
}
/>
onSelectChat=
{
handleSelectChat
}
onNewChat=
{
handleNewChat
}
/>
</
div
>
{
/* Main content */
}
{
/* Main */
}
<
div
className=
"flex-1 flex flex-col min-w-0"
>
<
div
className=
"flex-1 flex flex-col min-h-0 min-w-0"
>
{
/* Mobile header */
}
{
/* Top bar */
}
<
div
className=
"flex items-center gap-3 px-4 py-3 border-b border-anton-border bg-anton-surface lg:hidden"
>
<
div
className=
"border-b border-anton-border bg-anton-surface px-3 py-2 flex items-center gap-2"
>
<
button
<
button
onClick=
{
()
=>
setSidebarOpen
(
true
)
}
className=
"sm:hidden p-1.5 rounded-lg text-anton-muted hover:text-white hover:bg-anton-card transition"
>
onClick=
{
()
=>
dispatch
({
type
:
"SET_SIDEBAR_OPEN"
,
open
:
true
})
}
<
svg
width=
"20"
height=
"20"
viewBox=
"0 0 24 24"
fill=
"none"
stroke=
"currentColor"
strokeWidth=
"2"
strokeLinecap=
"round"
><
line
x1=
"3"
y1=
"6"
x2=
"21"
y2=
"6"
/><
line
x1=
"3"
y1=
"12"
x2=
"21"
y2=
"12"
/><
line
x1=
"3"
y1=
"18"
x2=
"21"
y2=
"18"
/></
svg
>
className=
"p-1.5 rounded-lg text-anton-muted hover:text-white hover:bg-anton-card transition"
>
<
Menu
size=
{
20
}
/>
</
button
>
</
button
>
<
div
className=
"flex-1 min-w-0"
>
<
div
className=
"w-7 h-7 rounded-lg bg-gradient-to-br from-anton-accent to-red-600 flex items-center justify-center"
>
<
h1
className=
"text-sm font-semibold text-white truncate"
>
<
Flame
size=
{
14
}
className=
"text-white"
/>
{
state
.
chats
.
find
((
c
)
=>
c
.
id
===
activeChatId
)?.
title
||
"Son of Anton"
}
</
h1
>
</
div
>
</
div
>
<
span
className=
"text-sm font-semibold text-white truncate flex-1"
>
{
state
.
chats
.
find
((
c
)
=>
c
.
id
===
activeChatId
)?.
title
||
"Son of Anton"
}
</
span
>
<
button
<
button
onClick=
{
handleNewChat
}
onClick=
{
()
=>
navigate
(
"/knowledge"
)
}
className=
"p-1.5 rounded-lg text-anton-muted hover:text-white hover:bg-anton-card transition"
className=
"flex items-center gap-1 px-2 py-1 rounded-lg text-xs text-anton-muted hover:text-green-400 hover:bg-green-500/10 transition"
title=
"Knowledge Bases"
>
>
<
Plus
size=
{
20
}
/>
<
BookOpen
size=
{
14
}
/>
<
span
className=
"hidden sm:inline"
>
Knowledge
</
span
>
</
button
>
</
button
>
{
state
.
user
?.
role
===
"superadmin"
&&
(
<
button
onClick=
{
()
=>
navigate
(
"/admin"
)
}
className=
"flex items-center gap-1 px-2 py-1 rounded-lg text-xs text-anton-muted hover:text-anton-accent hover:bg-anton-accent/10 transition"
title=
"Admin Panel"
>
<
Shield
size=
{
14
}
/>
<
span
className=
"hidden sm:inline"
>
Admin
</
span
>
</
button
>
)
}
</
div
>
</
div
>
{
/* Chat
or empty state
*/
}
{
/* Chat
area
*/
}
{
activeChatId
?
(
{
activeChatId
?
(
<
ChatView
chatId=
{
activeChatId
}
/>
<
ChatView
chatId=
{
activeChatId
}
/>
)
:
(
)
:
(
<
div
className=
"flex-1 flex items-center justify-center
p-8
"
>
<
div
className=
"flex-1 flex items-center justify-center"
>
<
div
className=
"text-center
max-w-md
"
>
<
div
className=
"text-center"
>
<
div
className=
"w-
20 h-20 rounded-2xl bg-gradient-to-br from-anton-accent to-red-600 flex items-center justify-center mx-auto mb-6
shadow-lg shadow-anton-accent/20"
>
<
div
className=
"w-
16 h-16 rounded-2xl bg-gradient-to-br from-anton-accent to-red-600 flex items-center justify-center mx-auto mb-4
shadow-lg shadow-anton-accent/20"
>
<
Flame
size=
{
40
}
className=
"text-white"
/>
<
Flame
size=
{
32
}
className=
"text-white"
/>
</
div
>
</
div
>
<
h2
className=
"text-2xl font-bold text-white mb-2"
>
Son of Anton
</
h2
>
<
h2
className=
"text-xl font-bold text-white mb-2"
>
Son of Anton
</
h2
>
<
p
className=
"text-anton-muted mb-6"
>
Avatar of All Elements of Code
</
p
>
<
p
className=
"text-anton-muted text-sm mb-6"
>
Avatar of All Elements of Code
</
p
>
<
button
<
button
onClick=
{
handleNewChat
}
className=
"px-6 py-2.5 rounded-xl bg-anton-accent text-white font-medium hover:opacity-80 transition"
>
onClick=
{
handleNewChat
}
Start a Chat
className=
"inline-flex items-center gap-2 px-6 py-3 bg-anton-accent text-white rounded-xl hover:opacity-90 transition font-medium"
>
<
MessageSquare
size=
{
18
}
/>
Start a conversation
</
button
>
</
button
>
</
div
>
</
div
>
</
div
>
</
div
>
...
...
frontend/src/pages/KnowledgePage.jsx
0 → 100644
View file @
e636c018
This diff is collapsed.
Click to expand it.
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