Skip to content

Commit

Permalink
Merge pull request #13 from hm-edu/update
Browse files Browse the repository at this point in the history
Update
  • Loading branch information
PandorasActorMS authored Oct 1, 2024
2 parents 43f7a76 + da313b8 commit 0779d77
Show file tree
Hide file tree
Showing 27 changed files with 1,042 additions and 336 deletions.
5 changes: 5 additions & 0 deletions Changelog.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
CHANGE LOG:

31.07.2024

- Open Source Models Connection (API and Endpoint adjustable from .env)
- Chatlog is saved on the localstorage

16.04.2024 Changelog – HAWKI V1.

- Multi language package added.
Expand Down
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,10 @@ Display of mathematical formulas, LaTex and improvement of syntax highlighting.

Dark Mode for our night owls.

System prompts can now be viewed transparently.
System prompts can now be viewed and edited.

In the new version each room's chatlog is saved by default and should be deleted before starting a new chat.


### Security updates

Expand Down Expand Up @@ -72,6 +75,7 @@ The new version also supports the Shibboleth for user authentication. Define you

To generate answers HAWKI uses the Open AI api. Follow the instructions on https://platform.openai.com/docs/introduction to generate an API key and paste it in the configuration file like instructed in chapter [Configuration](#configuration).


## Configuration

To get started you need to add a configuration file to the project first. Copy the file ".env.example" from the root directory and rename it to ".env". Replace the example values in it with your own configuration. A detailed description of all values is listed below.
Expand All @@ -85,20 +89,25 @@ To get started you need to add a configuration file to the project first. Copy t
| LDAP_SEARCH_DN | string | "ou=...,dc=..." | Distinguished name that is used for authenticating users. |
| LDAP_PORT | string | "..." | The LDAP port. |
| LDAP_FILTER | string | "..." | LDAP Filter. Choose the filter based on your LDAP configuration. See .env.example for more details.|
| LDAP_DEFAULT_INITIALS | string | "ABC" | User initials to use for every user. If not set, try to compute initials from LDAP displayname.|
| SHIBBOLET_LOGIN_PATH | string | "..." | Path to shibboleth login page. |
| SHIBBOLET_LOGIN_PAGE | string | "..." | Shibboleth login page. |
| OIDC_IDP | string | "https://...." | URL of the Identity provider supporting OpenID Connect. |
| OIDC_CLIENT_ID | string | "..." | Client Id for this application in Identity provider. |
| OIDC_CLIENT_SECRET | string | "..." | Secret key for OpenID Connect.
| OIDC_LOGOUT_URI | string | "https://...." | URL to logout from Identity provider |
| OPENAI_API_URL | string | "https://api.openai.com/v1/chat/completions" | Open AI URL |
| MODEL_SELECTOR_ACTIVATION | string | "true" | Set to true to activate dropdown. Deactivated Dropdown will force Gpt-4-0 as default model. |
| OPENAI_API_URL | string | "https://api.openai.com/v1/chat/completions" | Open AI Endpoint URL |
| OPENAI_API_KEY | string | sk-... | Open AI Api key |
| GWDG_API_URL | string | "https://api.openai.com/v1/chat/completions" | GWDG Endpoint URL |
| GWDG_API_KEY | string | | GWDG Api key |
| IMPRINT_LOCATION | string | https://your-university/imprint | A link to your imprint. Alternatively you can replace the file index.php under /impressum with your own html/ php of your imprint. |
| PRIVACY_LOCATION | string | https://your-university/privacy-policy | A link to your privacy policy. Alternatively you can replace the file index.php under /datenschutz with your own html/ php of your privacy policy. |
| TESTUSER | string | "tester" | Set value for testing purposes. Leave TESTUSER and TESTPASSWORD empty or comment them out to disable test user. |
| TESTPASSWORD | string | "superlangespasswort123" | Set value for testing purposes. Leave TESTUSER and TESTPASSWORD empty or comment them out to disable test user. |
| FAVICON_URI | string | "https://...." | Link to favicon |
| DEFAULT_LANGUAGE | string | "de_DE"/ "en_US"/ "es_ES"/ "fr_FR"/ "it_IT" | Default website language. Only applicable if the user has not previously changed the language or their browser language is not one of the supported languages. Current supported languages: 'de_DE', 'en_US', 'es_ES', 'fr_FR', 'it_IT' |
| CHATLOG_ENCRYPTION_SALT | string | ... | Set a strong salt specific to your application. This will be user to encrypt users' chatlogs in localstorage.|
## Web Server Configuration

There are a few things to keep in mind when publishing your HAWKI instance on a webserver.
Expand Down
7 changes: 6 additions & 1 deletion index.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,13 @@
include_once( LIBRARY_PATH . "stream-api.php");
}
exit;
case('/api/GWDG-api'):
if($_SERVER["REQUEST_METHOD"] == "POST"){
include_once( LIBRARY_PATH . "GWDG-stream-api.php");
}
exit;

default:
header("Location: /login");
header("Location: login");
exit();
}
11 changes: 11 additions & 0 deletions private/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,24 @@ LDAP_PORT=""
; (|(sAMAccountName=username)(mail=username))
; (|(uid=username)(mail=username))
LDAP_FILTER="(|(sAMAccountName=username)(mail=username))"
; LDAP_DEFAULT_INITIALS="ABC"

#Shibbolet
SHIBBOLETH_LOGIN_PATH="Shibboleth.sso/Login?target="
SHIBBOLETH_LOGIN_PAGE="login.php"
SHIBBOLETH_LOGOUT_URL=""

# Activates Model Selector Dropdown
MODEL_SELECTOR_ACTIVATION="true"

# Open Ai config
OPENAI_API_URL="https://api.openai.com/v1/chat/completions"
OPENAI_API_KEY="sk-..."

#GWDG Ai Config
GWDG_API_URL="https://chat-ai.academiccloud.de/v1/chat/completions"
GWDG_API_KEY=""

# Legal config
IMPRINT_LOCATION="https://my-univiersity.com/imprint"
PRIVACY_LOCATION="https://my-univiersity.com/ai-privacy-policy"
Expand Down Expand Up @@ -49,3 +57,6 @@ FAVICON_URI=""

# Default Language, leave blank to use default language of the user's browser.
DEFAULT_LANGUAGE="de_DE"

# fix salt for encryption users' chatlog in localstorage
CHATLOG_ENCRYPTION_SALT=""
64 changes: 64 additions & 0 deletions private/app/php/GWDG-stream-api.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php

session_start();
if (!isset($_SESSION['username'])) {
http_response_code(401);
exit;
}

header('Content-Type: application/json');
header('Cache-Control: no-cache');
header('Connection: keep-alive');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Headers: Content-Type');
header('Access-Control-Allow-Methods: POST, GET, OPTIONS');

// Add this block to handle preflight requests
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(204);
exit;
}

$_SESSION['last_activity'] = time();

// API configuration
$apiUrl = isset($env) ? $env['GWDG_API_URL'] : getenv('OPENAI_API_URL');
$apiKey = isset($env) ? $env['GWDG_API_KEY'] : getenv('OPENAI_API_KEY');

$requestPayload = file_get_contents('php://input');
// Decode the JSON payload into an associative array
$decodedPayload = json_decode($requestPayload, true);

// Add temperature and max_tokens keys
$decodedPayload['temperature'] = 0;
$decodedPayload['max_tokens'] = 1000;

// Encode the modified array back to JSON
$requestPayload = json_encode($decodedPayload);


$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $apiUrl);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $requestPayload);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json',
'Accept: application/json'
]);
curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) {
echo $data;
// ob_flush();
flush();
return strlen($data);
});

curl_exec($ch);

if (curl_errno($ch)) {
echo 'Error:' . curl_error($ch);
}

curl_close($ch);
16 changes: 11 additions & 5 deletions private/app/php/auth.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
//REGENERATE SESSION ID
session_regenerate_id();

header("Location: /interface");
header("Location: interface");
exit;
} else {
// Redirect user to shibboleth login page.
Expand Down Expand Up @@ -121,10 +121,16 @@ function handleLdapLogin($username, $password){
// Close LDAP connection
ldap_close($ldapConn);

// Extract initials from user's display name
$name = $info[0]["displayname"][0];
$parts = explode(", ", $name);
$initials = substr($parts[1], 0, 1) . substr($parts[0], 0, 1);
// Get username
if (isset($env['LDAP_DEFAULT_INITIALS']) && !empty($env['LDAP_DEFAULT_INITIALS'])) {
// Use default initials
$initials = $env['LDAP_DEFAULT_INITIALS'];
} else {
// Extract initials from user's display name
$name = $info[0]["displayname"][0];
$parts = explode(", ", $name);
$initials = substr($parts[1], 0, 1) . substr($parts[0], 0, 1);
}

// Set session variables
$_SESSION['username'] = $initials;
Expand Down
151 changes: 151 additions & 0 deletions private/app/php/chatlog_management.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
<?php

$encryptionSalt = isset($env) ? $env['CHATLOG_ENCRYPTION_SALT'] : getenv('CHATLOG_ENCRYPTION_SALT');
$userSpecificSalt = $encryptionSalt . $_SESSION['username'];

?>

<script>

function saveMessagesToLocalStorage() {
const username = '<?= htmlspecialchars($_SESSION['username']) ?>'; // Use the PHP variable
const messagesElement = document.querySelector(".messages");
const messageElements = messagesElement.querySelectorAll(".message");

let archiveObject = { messages: [] };

messageElements.forEach(messageElement => {
let messageObject = {};
messageObject.role = messageElement.dataset.role;

if(messageElement.dataset.role === 'assistant'){
messageObject.content = messageElement.querySelector(".message-text").getAttribute('rawContent');
}else{
messageObject.content = messageElement.querySelector(".message-text").textContent;
}

archiveObject.messages.push(messageObject);
});

// Convert messages to string
const messageString = JSON.stringify(archiveObject.messages);
const compressedMessages = LZString.compressToUTF16(messageString);

const salt = '<?= htmlspecialchars($userSpecificSalt) ?>';

// Derive a key from the username
const key = CryptoJS.PBKDF2(username, CryptoJS.enc.Hex.parse(salt), {
keySize: 256 / 32,
iterations: 1000
});

// Encrypt the messages
const encrypted = CryptoJS.AES.encrypt(compressedMessages, key.toString());
const storageDate = Date.now();

const storagePackage = JSON.stringify({
encryptedData: encrypted.toString(),
storageDate: storageDate,
})

// Save encrypted data to local storage
localStorage.setItem('chatLog_' + username, storagePackage);
}



function loadMessagesFromLocalStorage() {

const username = '<?= htmlspecialchars($_SESSION['username']) ?>'; // Use the PHP variable
const storedData = localStorage.getItem('chatLog_' + username);
if(storedData === null){
return;
}
const parsedData = JSON.parse(storedData);
const encryptedData = parsedData.encryptedData;
const salt = '<?= htmlspecialchars($userSpecificSalt) ?>';

if (encryptedData) {
try {
// Derive the key from the username
const key = CryptoJS.PBKDF2(username, CryptoJS.enc.Hex.parse(salt), {
keySize: 256 / 32,
iterations: 1000
});

// Decrypt the messages
const decrypted = CryptoJS.AES.decrypt(encryptedData, key.toString());
const decryptedString = decrypted.toString(CryptoJS.enc.Utf8);
// Decompress the messages
const decompressedString = LZString.decompressFromUTF16(decryptedString)
const messages = JSON.parse(decompressedString);

if(messages != null){
document.querySelector('.limitations')?.remove();
}

messages.forEach(message => {

const messagesElement = document.querySelector(".messages");
const messageTemplate = document.querySelector('#message');
const messageElement = messageTemplate.content.cloneNode(true);

messageElement.querySelector(".message").dataset.role = message.role;

if(message.role == "assistant"){
messageElement.querySelector(".message-icon").textContent = "AI";
messageElement.querySelector(".message-text").setAttribute('rawContent', message.content);
//FORMAT RAW TEXT AGAIN
const formattedContent = FormatWholeMessage(message.content);
messageElement.querySelector(".message-text").innerHTML = formattedContent;

} else{
messageElement.querySelector(".message-text").innerHTML = message.content;
messageElement.querySelector(".message-icon").textContent = '<?= htmlspecialchars($_SESSION['username']) ?>';
messageElement.querySelector(".message").classList.add("me");
}

messagesElement.appendChild(messageElement);
hljs.highlightAll();
FormatMathFormulas();
scrollToLast(true);

});
} catch (error) {
console.error("Failed to decrypt or parse messages:", error);
}
}
}


function cleanupStoredLogs(){
const items = {};
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key.startsWith("chatLog_")) {
const storedData = localStorage.getItem(localStorage.key(i));
const parsedData = JSON.parse(storedData);

// check if the stored data is older than one week
if(Date.now() > parsedData.storageDate + 7 * 24 * 60 * 60 * 1000){
localStorage.removeItem(localStorage.key(i));
}
}
}
}


function deleteChatLog(){
const username = '<?= htmlspecialchars($_SESSION['username']) ?>'; // Use the PHP variable
localStorage.removeItem('chatLog_' + username);
const chatBtn = document.querySelector("#chatMenuButton");
load(chatBtn ,'chat.php');
}
function openDeletePanel(){
document.getElementById('delete-chat-confirm').style.display = "flex";
}
function cancelDelete(){
document.getElementById('delete-chat-confirm').style.display = "none";
}

</script>
4 changes: 2 additions & 2 deletions private/app/php/submit_vote.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,9 @@

// Find the feedback entry by ID
$foundFeedback = null;
foreach ($feedbackData as $feedback) {
foreach ($feedbackData as &$feedback) {
if ($feedback['id'] === $data['id']) {
$foundFeedback = $feedback;
$foundFeedback = &$feedback;
break;
}
}
Expand Down
Loading

0 comments on commit 0779d77

Please sign in to comment.