Compare commits
4 commits
102fc816f8
...
d642bb0a39
Author | SHA1 | Date | |
---|---|---|---|
d642bb0a39 | |||
62c54dc3e6 | |||
833a446899 | |||
a994eacd2d |
|
@ -2,7 +2,7 @@ import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.responses import HTMLResponse, RedirectResponse, FileResponse
|
from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from starlette.middleware.sessions import SessionMiddleware
|
from starlette.middleware.sessions import SessionMiddleware
|
||||||
from starlette.requests import Request
|
from starlette.requests import Request
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
from fastapi.responses import PlainTextResponse
|
from fastapi.responses import PlainTextResponse
|
||||||
|
|
||||||
from hellocomputer.sessions import SessionDB
|
|
||||||
from hellocomputer.db import StorageEngines
|
from hellocomputer.db import StorageEngines
|
||||||
from hellocomputer.extraction import extract_code_block
|
from hellocomputer.extraction import extract_code_block
|
||||||
|
from hellocomputer.sessions import SessionDB
|
||||||
|
|
||||||
from ..config import settings
|
from ..config import settings
|
||||||
from ..models import Chat
|
from ..models import Chat
|
||||||
|
|
|
@ -2,9 +2,10 @@ from authlib.integrations.starlette_client import OAuth, OAuthError
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||||
from starlette.requests import Request
|
from starlette.requests import Request
|
||||||
|
|
||||||
from hellocomputer.config import settings
|
from hellocomputer.config import settings
|
||||||
from hellocomputer.users import UserDB
|
|
||||||
from hellocomputer.db import StorageEngines
|
from hellocomputer.db import StorageEngines
|
||||||
|
from hellocomputer.users import UserDB
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
|
@ -3,10 +3,12 @@ import aiofiles
|
||||||
# import s3fs
|
# import s3fs
|
||||||
from fastapi import APIRouter, File, UploadFile
|
from fastapi import APIRouter, File, UploadFile
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
|
from starlette.requests import Request
|
||||||
|
|
||||||
from ..sessions import SessionDB
|
|
||||||
from ..config import settings
|
from ..config import settings
|
||||||
from ..db import StorageEngines
|
from ..db import StorageEngines
|
||||||
|
from ..sessions import SessionDB
|
||||||
|
from ..users import OwnershipDB
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
@ -21,7 +23,7 @@ router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@router.post("/upload", tags=["files"])
|
@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:
|
async with aiofiles.tempfile.NamedTemporaryFile("wb") as f:
|
||||||
content = await file.read()
|
content = await file.read()
|
||||||
await f.write(content)
|
await f.write(content)
|
||||||
|
@ -39,6 +41,13 @@ async def upload_file(file: UploadFile = File(...), sid: str = ""):
|
||||||
.dump()
|
.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(
|
return JSONResponse(
|
||||||
content={"message": "File uploaded successfully"}, status_code=200
|
content={"message": "File uploaded successfully"}, status_code=200
|
||||||
)
|
)
|
||||||
|
|
|
@ -3,8 +3,11 @@ from uuid import uuid4
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
from fastapi.responses import PlainTextResponse
|
from fastapi.responses import PlainTextResponse
|
||||||
from starlette.requests import Request
|
from starlette.requests import Request
|
||||||
from hellocomputer.users import OwnershipDB
|
from typing import List
|
||||||
|
|
||||||
from hellocomputer.db import StorageEngines
|
from hellocomputer.db import StorageEngines
|
||||||
|
from hellocomputer.users import OwnershipDB
|
||||||
|
|
||||||
from ..config import settings
|
from ..config import settings
|
||||||
|
|
||||||
# Scheme for the Authorization header
|
# Scheme for the Authorization header
|
||||||
|
@ -13,17 +16,8 @@ router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@router.get("/new_session")
|
@router.get("/new_session")
|
||||||
async def get_new_session(request: Request) -> str:
|
async def get_new_session() -> str:
|
||||||
user_email = request.session.get("user").get("email")
|
return str(uuid4())
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/greetings", response_class=PlainTextResponse)
|
@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 "
|
"Hi! I'm a helpful assistant. Please upload or select a file "
|
||||||
"and I'll try to analyze it following your orders"
|
"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)
|
||||||
|
|
49
src/hellocomputer/static/about.html
Normal file
49
src/hellocomputer/static/about.html
Normal 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>
|
|
@ -63,9 +63,8 @@
|
||||||
<label for="datasetLabel" class="form-label">Sesson name</label>
|
<label for="datasetLabel" class="form-label">Sesson name</label>
|
||||||
<input type="text" class="form-control" id="datasetLabel"
|
<input type="text" class="form-control" id="datasetLabel"
|
||||||
aria-describedby="labelHelp">
|
aria-describedby="labelHelp">
|
||||||
<div id="labelHelp" class="form-text">You'll be able to recover this file in the
|
<div id="labelHelp" class="form-text">
|
||||||
future
|
You'll be able to recover this file in the future with this name
|
||||||
with this name
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
|
@ -86,8 +85,9 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Button trigger modal -->
|
<!-- Button trigger modal -->
|
||||||
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#staticBackdrop">
|
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#staticBackdrop"
|
||||||
Or load an existing session
|
id="loadSessionsButton">
|
||||||
|
Load a session
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Modal -->
|
<!-- Modal -->
|
||||||
|
@ -96,15 +96,17 @@
|
||||||
<div class="modal-dialog modal-dialog-scrollable">
|
<div class="modal-dialog modal-dialog-scrollable">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<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"
|
<button type="button" class="btn-close" data-bs-dismiss="modal"
|
||||||
aria-label="Close"></button>
|
aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body" id="userSessions">
|
||||||
<p>Current sessions</p>
|
<ul id="userSessions">
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -35,6 +35,8 @@
|
||||||
<h1 class="h3 mb-3 fw-normal techie-font">Hola, computer!</h1>
|
<h1 class="h3 mb-3 fw-normal techie-font">Hola, computer!</h1>
|
||||||
<img src="/app/img/assistant.webp" alt="Logo" class="logo img-fluid">
|
<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>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ $('#menu-toggle').click(function (e) {
|
||||||
toggleMenuArrow(document.getElementById('menu-toggle'));
|
toggleMenuArrow(document.getElementById('menu-toggle'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Hide sidebar on mobile devices
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
console.log('Width: ' + window.innerWidth + ' Height: ' + window.innerHeight);
|
console.log('Width: ' + window.innerWidth + ' Height: ' + window.innerHeight);
|
||||||
if ((window.innerWidth <= 800) && (window.innerHeight <= 600)) {
|
if ((window.innerWidth <= 800) && (window.innerHeight <= 600)) {
|
||||||
|
@ -27,6 +28,7 @@ const textarea = document.getElementById('chatTextarea');
|
||||||
const sendButton = document.getElementById('sendButton');
|
const sendButton = document.getElementById('sendButton');
|
||||||
const chatMessages = document.querySelector('.chat-messages');
|
const chatMessages = document.querySelector('.chat-messages');
|
||||||
|
|
||||||
|
// Auto resize textarea
|
||||||
textarea.addEventListener('input', function () {
|
textarea.addEventListener('input', function () {
|
||||||
this.style.height = 'auto';
|
this.style.height = 'auto';
|
||||||
this.style.height = (this.scrollHeight <= 150 ? this.scrollHeight : 150) + 'px';
|
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) {
|
function addAIMessage(messageContent) {
|
||||||
const newMessage = document.createElement('div');
|
const newMessage = document.createElement('div');
|
||||||
newMessage.classList.add('message', 'bg-white', 'p-2', 'mb-2', 'rounded');
|
newMessage.classList.add('message', 'bg-white', 'p-2', 'mb-2', 'rounded');
|
||||||
|
@ -123,6 +126,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||||
fetchGreeting();
|
fetchGreeting();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Function upload the data file
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
const fileInput = document.getElementById('inputGroupFile01');
|
const fileInput = document.getElementById('inputGroupFile01');
|
||||||
const uploadButton = document.getElementById('uploadButton');
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
|
@ -1,11 +1,12 @@
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import List
|
||||||
from uuid import UUID, uuid4
|
from uuid import UUID, uuid4
|
||||||
|
|
||||||
import duckdb
|
import duckdb
|
||||||
import polars as pl
|
import polars as pl
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from .db import DDB, StorageEngines
|
from .db import DDB, StorageEngines
|
||||||
|
|
||||||
|
@ -79,7 +80,7 @@ class OwnershipDB(DDB):
|
||||||
'{sid}' as sid,
|
'{sid}' as sid,
|
||||||
'{now}' as timestamp
|
'{now}' as timestamp
|
||||||
)
|
)
|
||||||
TO '{self.path_prefix}/{record_id}.csv' (FORMAT JSON)"""
|
TO '{self.path_prefix}/{record_id}.csv'"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.db.sql(query)
|
self.db.sql(query)
|
||||||
|
@ -88,3 +89,25 @@ class OwnershipDB(DDB):
|
||||||
self.db.sql(query)
|
self.db.sql(query)
|
||||||
|
|
||||||
return sid
|
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 []
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import hellocomputer
|
import hellocomputer
|
||||||
from hellocomputer.sessions import SessionDB
|
|
||||||
from hellocomputer.db import StorageEngines
|
from hellocomputer.db import StorageEngines
|
||||||
|
from hellocomputer.sessions import SessionDB
|
||||||
|
|
||||||
TEST_STORAGE = StorageEngines.local
|
TEST_STORAGE = StorageEngines.local
|
||||||
TEST_XLS_PATH = (
|
TEST_XLS_PATH = (
|
||||||
|
|
|
@ -2,11 +2,11 @@ from pathlib import Path
|
||||||
|
|
||||||
import hellocomputer
|
import hellocomputer
|
||||||
import pytest
|
import pytest
|
||||||
from hellocomputer.sessions import SessionDB
|
|
||||||
from hellocomputer.config import settings
|
from hellocomputer.config import settings
|
||||||
from hellocomputer.db import StorageEngines
|
from hellocomputer.db import StorageEngines
|
||||||
from hellocomputer.extraction import extract_code_block
|
from hellocomputer.extraction import extract_code_block
|
||||||
from hellocomputer.models import Chat
|
from hellocomputer.models import Chat
|
||||||
|
from hellocomputer.sessions import SessionDB
|
||||||
|
|
||||||
TEST_XLS_PATH = (
|
TEST_XLS_PATH = (
|
||||||
Path(hellocomputer.__file__).parents[2]
|
Path(hellocomputer.__file__).parents[2]
|
||||||
|
|
|
@ -2,7 +2,7 @@ from pathlib import Path
|
||||||
|
|
||||||
import hellocomputer
|
import hellocomputer
|
||||||
from hellocomputer.db import StorageEngines
|
from hellocomputer.db import StorageEngines
|
||||||
from hellocomputer.users import UserDB, OwnershipDB
|
from hellocomputer.users import OwnershipDB, UserDB
|
||||||
|
|
||||||
TEST_STORAGE = StorageEngines.local
|
TEST_STORAGE = StorageEngines.local
|
||||||
TEST_OUTPUT_FOLDER = Path(hellocomputer.__file__).parents[2] / "test" / "output"
|
TEST_OUTPUT_FOLDER = Path(hellocomputer.__file__).parents[2] / "test" / "output"
|
||||||
|
@ -28,7 +28,13 @@ def test_user_exists():
|
||||||
def test_assign_owner():
|
def test_assign_owner():
|
||||||
assert (
|
assert (
|
||||||
OwnershipDB(storage_engine=TEST_STORAGE, path=TEST_OUTPUT_FOLDER).set_ownersip(
|
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"]
|
||||||
|
|
Loading…
Reference in a new issue