Compare commits

...

4 commits

Author SHA1 Message Date
Guillem Borrell d642bb0a39 Add about
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2024-06-14 21:58:14 +02:00
Guillem Borrell 62c54dc3e6 List sessions 2024-06-12 22:22:26 +02:00
Guillem Borrell 833a446899 Save session when file is uploaded 2024-06-12 12:45:09 +02:00
Guillem Borrell a994eacd2d Get available sessions 2024-06-12 11:04:28 +02:00
13 changed files with 164 additions and 33 deletions

View file

@ -2,7 +2,7 @@ import json
from pathlib import Path
from fastapi import FastAPI
from fastapi.responses import HTMLResponse, RedirectResponse, FileResponse
from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse
from fastapi.staticfiles import StaticFiles
from starlette.middleware.sessions import SessionMiddleware
from starlette.requests import Request

View file

@ -1,9 +1,9 @@
from fastapi import APIRouter
from fastapi.responses import PlainTextResponse
from hellocomputer.sessions import SessionDB
from hellocomputer.db import StorageEngines
from hellocomputer.extraction import extract_code_block
from hellocomputer.sessions import SessionDB
from ..config import settings
from ..models import Chat

View file

@ -2,9 +2,10 @@ from authlib.integrations.starlette_client import OAuth, OAuthError
from fastapi import APIRouter
from fastapi.responses import HTMLResponse, RedirectResponse
from starlette.requests import Request
from hellocomputer.config import settings
from hellocomputer.users import UserDB
from hellocomputer.db import StorageEngines
from hellocomputer.users import UserDB
router = APIRouter()

View file

@ -3,10 +3,12 @@ import aiofiles
# import s3fs
from fastapi import APIRouter, File, UploadFile
from fastapi.responses import JSONResponse
from starlette.requests import Request
from ..sessions import SessionDB
from ..config import settings
from ..db import StorageEngines
from ..sessions import SessionDB
from ..users import OwnershipDB
router = APIRouter()
@ -21,7 +23,7 @@ router = APIRouter()
@router.post("/upload", tags=["files"])
async def upload_file(file: UploadFile = File(...), sid: str = ""):
async def upload_file(request: Request, file: UploadFile = File(...), sid: str = ""):
async with aiofiles.tempfile.NamedTemporaryFile("wb") as f:
content = await file.read()
await f.write(content)
@ -39,6 +41,13 @@ async def upload_file(file: UploadFile = File(...), sid: str = ""):
.dump()
)
OwnershipDB(
StorageEngines.gcs,
gcs_access=settings.gcs_access,
gcs_secret=settings.gcs_secret,
bucket=settings.gcs_bucketname,
).set_ownersip(request.session.get("user").get("email"), sid)
return JSONResponse(
content={"message": "File uploaded successfully"}, status_code=200
)

View file

@ -3,8 +3,11 @@ from uuid import uuid4
from fastapi import APIRouter
from fastapi.responses import PlainTextResponse
from starlette.requests import Request
from hellocomputer.users import OwnershipDB
from typing import List
from hellocomputer.db import StorageEngines
from hellocomputer.users import OwnershipDB
from ..config import settings
# Scheme for the Authorization header
@ -13,17 +16,8 @@ router = APIRouter()
@router.get("/new_session")
async def get_new_session(request: Request) -> str:
user_email = request.session.get("user").get("email")
ownership = OwnershipDB(
StorageEngines.gcs,
gcs_access=settings.gcs_access,
gcs_secret=settings.gcs_secret,
bucket=settings.gcs_bucketname,
)
sid = str(uuid4())
return ownership.set_ownersip(user_email, sid)
async def get_new_session() -> str:
return str(uuid4())
@router.get("/greetings", response_class=PlainTextResponse)
@ -32,3 +26,15 @@ async def get_greeting() -> str:
"Hi! I'm a helpful assistant. Please upload or select a file "
"and I'll try to analyze it following your orders"
)
@router.get("/sessions")
async def get_sessions(request: Request) -> List[str]:
user_email = request.session.get("user").get("email")
ownership = OwnershipDB(
StorageEngines.gcs,
gcs_access=settings.gcs_access,
gcs_secret=settings.gcs_secret,
bucket=settings.gcs_bucketname,
)
return ownership.sessions(user_email)

View file

@ -0,0 +1,49 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hola, computer!</title>
<link rel="icon" type="image/x-icon" href="/app/img/favicon.ico">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Share+Tech+Mono&display=swap" rel="stylesheet">
<style>
.login-container {
max-width: 400px;
margin: 0 auto;
padding: 50px 0;
}
.logo {
display: block;
margin: 0 auto 20px auto;
}
.techie-font {
font-family: 'Share Tech Mono', monospace;
font-size: 24px;
text-align: center;
margin-bottom: 20px;
}
</style>
</head>
<body>
<div class="container">
<div class="login-container text-center">
<h1 class="h3 mb-3 fw-normal techie-font">Hola, computer!</h1>
<img src="/app/img/assistant.webp" alt="Logo" class="logo img-fluid">
<p class="techie-font">
Hola, computer! is a web assistant that allows you to query excel files using natural language. It may
not be as powerful as Excel, but it has an efficient query backend that can process your data faster
and more efficiently than Excel.
</p>
<a href="/"><button class="btn btn-secondary w-100">Back</button></a>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View file

@ -63,9 +63,8 @@
<label for="datasetLabel" class="form-label">Sesson name</label>
<input type="text" class="form-control" id="datasetLabel"
aria-describedby="labelHelp">
<div id="labelHelp" class="form-text">You'll be able to recover this file in the
future
with this name
<div id="labelHelp" class="form-text">
You'll be able to recover this file in the future with this name
</div>
</div>
<div class="modal-body">
@ -86,8 +85,9 @@
</div>
<!-- Button trigger modal -->
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#staticBackdrop">
Or load an existing session
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#staticBackdrop"
id="loadSessionsButton">
Load a session
</button>
<!-- Modal -->
@ -96,15 +96,17 @@
<div class="modal-dialog modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="staticBackdropLabel">Current sessions</h5>
<h5 class="modal-title" id="staticBackdropLabel">Available sessions</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"
aria-label="Close"></button>
</div>
<div class="modal-body">
<p>Current sessions</p>
<div class="modal-body" id="userSessions">
<ul id="userSessions">
</ul>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"
id="sessionCloseButton">Close</button>
</div>
</div>
</div>

View file

@ -35,6 +35,8 @@
<h1 class="h3 mb-3 fw-normal techie-font">Hola, computer!</h1>
<img src="/app/img/assistant.webp" alt="Logo" class="logo img-fluid">
<a href="/login"><button type="submit" class="btn btn-primary w-100">Login</button></a>
<p></p>
<a href="/app/about.html"><button class="btn btn-secondary w-100">About</button></a>
</div>
</div>

View file

@ -13,6 +13,7 @@ $('#menu-toggle').click(function (e) {
toggleMenuArrow(document.getElementById('menu-toggle'));
});
// Hide sidebar on mobile devices
document.addEventListener("DOMContentLoaded", function () {
console.log('Width: ' + window.innerWidth + ' Height: ' + window.innerHeight);
if ((window.innerWidth <= 800) && (window.innerHeight <= 600)) {
@ -27,6 +28,7 @@ const textarea = document.getElementById('chatTextarea');
const sendButton = document.getElementById('sendButton');
const chatMessages = document.querySelector('.chat-messages');
// Auto resize textarea
textarea.addEventListener('input', function () {
this.style.height = 'auto';
this.style.height = (this.scrollHeight <= 150 ? this.scrollHeight : 150) + 'px';
@ -53,6 +55,7 @@ async function fetchResponse(message, newMessage) {
}
}
// Function to add AI message
function addAIMessage(messageContent) {
const newMessage = document.createElement('div');
newMessage.classList.add('message', 'bg-white', 'p-2', 'mb-2', 'rounded');
@ -123,6 +126,7 @@ document.addEventListener("DOMContentLoaded", function () {
fetchGreeting();
});
// Function upload the data file
document.addEventListener("DOMContentLoaded", function () {
const fileInput = document.getElementById('inputGroupFile01');
const uploadButton = document.getElementById('uploadButton');
@ -158,3 +162,32 @@ document.addEventListener("DOMContentLoaded", function () {
}
});
});
// Function to get the user sessions
document.addEventListener("DOMContentLoaded", function () {
const sessionsButton = document.getElementById('loadSessionsButton');
const sessions = document.getElementById('userSessions');
sessionsButton.addEventListener('click', async function fetchSessions() {
try {
const response = await fetch('/sessions');
if (!response.ok) {
throw new Error('Network response was not ok ' + response.statusText);
}
const data = JSON.parse(await response.text());
sessions.innerHTML = '';
data.forEach(item => {
const listItem = document.createElement('li');
const button = document.createElement('button');
button.textContent = item;
button.addEventListener("click", function () { alert(`You clicked on ${item}`); });
listItem.appendChild(button);
sessions.appendChild(listItem);
});
} catch (error) {
sessions.innerHTML = 'Error: ' + error.message;
}
}
);
}
);

View file

@ -1,11 +1,12 @@
import json
import os
from datetime import datetime
from pathlib import Path
from typing import List
from uuid import UUID, uuid4
import duckdb
import polars as pl
from datetime import datetime
from .db import DDB, StorageEngines
@ -79,7 +80,7 @@ class OwnershipDB(DDB):
'{sid}' as sid,
'{now}' as timestamp
)
TO '{self.path_prefix}/{record_id}.csv' (FORMAT JSON)"""
TO '{self.path_prefix}/{record_id}.csv'"""
try:
self.db.sql(query)
@ -88,3 +89,25 @@ class OwnershipDB(DDB):
self.db.sql(query)
return sid
def sessions(self, user_email: str) -> List[str]:
try:
return (
self.db.sql(f"""
SELECT
sid
FROM
'{self.path_prefix}/*.csv'
WHERE
email = '{user_email}
ORDER BY
timestamp ASC
LIMIT 10'
""")
.pl()
.to_series()
.to_list()
)
# If the table does not exist
except duckdb.duckdb.IOException:
return []

View file

@ -1,8 +1,8 @@
from pathlib import Path
import hellocomputer
from hellocomputer.sessions import SessionDB
from hellocomputer.db import StorageEngines
from hellocomputer.sessions import SessionDB
TEST_STORAGE = StorageEngines.local
TEST_XLS_PATH = (

View file

@ -2,11 +2,11 @@ from pathlib import Path
import hellocomputer
import pytest
from hellocomputer.sessions import SessionDB
from hellocomputer.config import settings
from hellocomputer.db import StorageEngines
from hellocomputer.extraction import extract_code_block
from hellocomputer.models import Chat
from hellocomputer.sessions import SessionDB
TEST_XLS_PATH = (
Path(hellocomputer.__file__).parents[2]

View file

@ -2,7 +2,7 @@ from pathlib import Path
import hellocomputer
from hellocomputer.db import StorageEngines
from hellocomputer.users import UserDB, OwnershipDB
from hellocomputer.users import OwnershipDB, UserDB
TEST_STORAGE = StorageEngines.local
TEST_OUTPUT_FOLDER = Path(hellocomputer.__file__).parents[2] / "test" / "output"
@ -28,7 +28,13 @@ def test_user_exists():
def test_assign_owner():
assert (
OwnershipDB(storage_engine=TEST_STORAGE, path=TEST_OUTPUT_FOLDER).set_ownersip(
"something.something@something", "1234", "test"
"something.something@something", "testsession", "test"
)
== "1234"
== "testsession"
)
def test_get_sessions():
assert OwnershipDB(storage_engine=TEST_STORAGE, path=TEST_OUTPUT_FOLDER).sessions(
"something.something@something"
) == ["testsession"]